ООП на Java примерах

Автор: | 05.02.2018

Создание проекта Java
Простейшее оконное приложение
Оконное приложение с обработкой событий
Создание картинки в окне
Анимация изображения
Игровое приложение ”Snake”
Сетевые приложения:

Приложение “Date Server and Client” с одним клиентом
Приложение “Capitalization Server and Client” с несколькими клиентами
Игра для двух игроков “Крестики-нолики”

Полезные ссылки

Создание проекта Java

У Вас уже должны быть установлены среда разработки приложений NetBeans  и JDK (Java Development Kit).

Запустите NetBeans. В меню выберите File/NewProject/Java/ и введите pro1 в ответ на запрос имени проекта, а затем нажмите Finish. При этом будет создан файл Pro1.java с классом Pro1 и пустым методом main() в нем.

Добавим следующий код в этот метод:

for (int i=0;i<3; i++) {
System.out.printf ("%d Hi!\n",i);
}

Для запуска программы выберем в меню Run /Run Project. В нижней панели  отобразится результат работы программы:

Простейшее оконное приложение

Построим изучение основ языка Java на аналогиях, сравнивая его с языком C++. В начале рассмотрим программы, подобные простейшим MFC приложениям.

Начнем с простого, создадим программу, которая показывает пустое окно.

Исходный код программы:

MyWindowApp.java

import javax.swing.JFrame;
public class MyWindowApp extends JFrame {
// конструктор класса приложения
public MyWindowApp(){
super("My First Window"); // 2 конструктор базового класса
setBounds(100, 100, 300, 200); // 3 размер и положение окна
}

//Точка входа в программу — функция main
public static void main(String[] args) {
//Создаем экземпляр приложения,  при этом запускается конструктор
MyWindowApp app = new MyWindowApp();  // 1
app.setVisible(true); // 4 Приложение запущено!
}
}

Последовательность выполнения (обозначена 1-4) практически та же, что и для  простейшего оконного MFC приложения.  При разработке Java приложения  программист использует базовые классы, строит производные от них классы. Проект программы может содержать несколько классов. Один из классов проекта содержит функцию main, которая есть точкой входа в программу. Имя класса должно совпадать с именем файла, в котором класс описан.

Java – полностью объектно-ориентированный язык, даже в большей степени, чем C++. Функции и переменные, не привязанные к контексту какого-либо объекта, больше не присутствуют в системе. Примером есть функция main и объект приложения app, которые в Java приложении отнесены к создаваемому классу приложения. В MFC приложении отсутствует функция main (WinMain спрятана в MFC каркасе) и объект приложения создается как глобальная переменная.

Полностью исключена препроцессорная обработка. Операция включения в программу файлов-заголовков с описаниями классов (include) заменена на операцию import, которая читает подготовленные бинарные файлы с описанием классов. Для поддержки пользовательских интерфейсов язык Java содержит библиотеки AWT и Swing,  позволяющие  создавать  и  поддерживать  окна,  использовать  элементы управления (кнопки, меню, полосы прокрутки и др.), применять инструменты для создания  графических  приложений.

В Java отсутствуют  указатели, хотя все переменные объектного типа являются ссылками. Создаются такие переменные динамически – через оператор New. При этом исчезла необходимость явно управлять памятью вызовами функции free или оператором delete, поскольку в систему встроен автоматический «сборщик мусора». Он освобождает память, на которую больше нет ссылок.

Оконное приложение с обработкой событий

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

Вначале создадим метку (countLabel) а также две командные кнопки (addCrow и removeCrow) и разместим компоненты в окне:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class VoronCalc extends JFrame {
private int voron = 0;
private JLabel countLabel;
private JButton addCrow;
private JButton removeCrow;

public VoronCalc(){    //  2
super("Crow calculator");    
//Cоздаем метку
countLabel = new JLabel("Crows:" + voron);
//Cоздаем кнопки
addCrow = new JButton("Add Crow");
removeCrow = new JButton("Remove Crow");
//Подготавливаем панель
JPanel buttonsPanel = new JPanel(new FlowLayout());
//Расставляем компоненты по местам
add(countLabel, BorderLayout.NORTH);
buttonsPanel.add(addCrow);
buttonsPanel.add(removeCrow);
add(buttonsPanel, BorderLayout.SOUTH);
}

public static void main(String[] args) {
VoronCalc app = new VoronCalc();     // 1
app.setVisible(true);      // 3
app.pack(); // оптимальный размер окна
}
}

Посмотрите, как выполнялась аналогичная задача в  MFC приложении. Существенное отличие лишь в том, как созданные в окне компоненты удаляются. В  MFC приложении они удаляются в деструкторе оператором delete.  В Java приложении память, на которую больше нет ссылок, освобождается автоматически «сборщиком мусора».

 Добавление событий

Пришло время добавить немного интерактивности. Нам нужно сделать 3 вещи:

  1. Научить кнопку addCrow добавлять 1 к переменной voron.
  2. Научить кнопку removeCrow вычитать 1 из переменной voron.
  3. Научить кнопку countLabel обновлять свое значение в зависимости от содержимого переменной voron.

Исходный код программы приводится ниже.

VoronCalc.java

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;

public class VoronCalc extends JFrame {
private int voron = 0;
private JLabel countLabel;
private JButton addCrow;
private JButton removeCrow;

public VoronCalc() {
super("Crow calculator"); // конструктор базового класса
/* Подготавливаем компоненты объекта  */
countLabel = new JLabel("Crows:" + voron);
addCrow = new JButton("Add Crow");
removeCrow = new JButton("Remove Crow");
/* Подготавливаем временные компоненты  */
JPanel buttonsPanel = new JPanel(new FlowLayout());
/* Расставляем компоненты по местам  */
add(countLabel, BorderLayout.NORTH);
buttonsPanel.add(addCrow);
buttonsPanel.add(removeCrow);
add(buttonsPanel, BorderLayout.SOUTH);
initListeners();
}

private void initListeners() {
// Добавляем listener для кнопки addCrow
// У объекта нет имени, поскольку используется только один раз.
      addCrow.addActionListener(new ActionListener()
// У класса также нет имени - анонимный внутренний класс.
{
public void actionPerformed(ActionEvent e) {
voron = voron + 1;   /* Добавляем одну ворону */
updateCrowCounter(); /* Сообщение, что кол. ворон изм.*/
}
}
);

//Добавляем listener для кнопки removeCrow
removeCrow.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (voron > 0) {
voron = voron - 1;
updateCrowCounter(); /* Сообщ., что кол. ворон изм.*/
}
}
});
}

//Обновляем CrowCounter 
private void updateCrowCounter() {
countLabel.setText("Crows:" + voron);
}
public static void main(String[] args) {
VoronCalc app = new VoronCalc();
app.setVisible(true);
app.pack(); /* оптим. размер в завис. от содержимого окна  */
}
}

В MFC приложениях события идентифицировались именем константы в таблице-макросе, отнесенной к классу.  Такое описание не имело ничего общего с ООП.

В Java для определения события используются три действующих лица – объект-источник события, объект-слушатель события и объект-событие.

Мы создаем сначала кнопку (объект-источник). При вызове метода addActionListener создается объект класса ActionListener (слушатель).  При вызове обработчика события  (метод actionPerformed) создается объект класса ActionEvent (событие), в котором  объединены параметры события.

Объекты – источники событий должны быть объектами класса, который имеет методы для регистрации слушателя addXXXListener и отключения слушателя removeXXXListener. Здесь под XXX подразумевается некоторое имя события. Например, ActionListener или AWTEventListener, и т.д.

Один из подходов добавления событий состоит в том, чтобы создать для каждого компонента внутренний анонимный (без имени) класс непосредственно в методе addActionListener. Описание класса состоит только из определения метода actionPerformed. Синтаксис может показаться вам немного неуклюжим, но запомните его, в дальнейшем привыкните.

Создание картинки в окне

Для создания картинки необходимо в класс окна добавить панель – элемент класса, производный от класса  Jpanel. Объектами на панели могут быть подгружаемые картинки, либо рисунки, выполненные инструментами  Java 2D API.

Исходный код программы приводится ниже.

Donut.java

import javax.swing.JFrame;

public class Donut extends JFrame {
    public Donut() {
//создается элемент класса, производный от класс  Jpanel
        add(new Board());   // 2
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 4
        setSize(360, 310);
        setLocationRelativeTo(null);
        setTitle("Donut");
        setVisible(true);
    }

    public static void main(String[] args) {
        new Donut();  // 1
    }
}

Board.java

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import javax.swing.JPanel;

public class Board extends JPanel{
    public void paint(Graphics g)   // 3
    {
      super.paint(g); // статическая функция базового класса
      Graphics2D g2 = (Graphics2D) g;
      RenderingHints rh =
            new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                               RenderingHints.VALUE_ANTIALIAS_ON);
      rh.put(RenderingHints.KEY_RENDERING,
             RenderingHints.VALUE_RENDER_QUALITY);
      g2.setRenderingHints(rh);
      Dimension size = getSize();
      double w = size.getWidth();
      double h = size.getHeight();
      Ellipse2D e = new Ellipse2D.Double(0, 0, 80, 130);
      g2.setStroke(new BasicStroke(1));
      g2.setColor(Color.gray); 
      for (double deg = 0; deg < 360; deg += 5) {
AffineTransform at=AffineTransform.getTranslateInstance(w/2,h/2);
          at.rotate(Math.toRadians(deg)); 
          g2.draw(at.createTransformedShape(e));
        }
    }
}

Анимация изображения

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

Исходный код программы приводится ниже. Файл рисунка «star.png»  размещается в директории, где находятся файлы классов проекта.

Star.java

import javax.swing.JFrame;
public class Star extends JFrame {
public Star() {
add(new Board());    //  2
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(280, 240);
setLocationRelativeTo(null);
setTitle("Star");
setResizable(false);
setVisible(true);
}

public static void main(String[] args) {
new Star();      //  1
}
}

Board.java

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer; //импортируем  Swing Timer класс

// интерфейс ActionListener реализует метод actionPerformed()
public class Board extends JPanel implements ActionListener {
Image star;
Timer timer;
int x, y;
public Board() {    //   3
setBackground(Color.BLACK);
ImageIcon ii = new ImageIcon(this.getClass().getResource("star.png"));
star = ii.getImage();
setDoubleBuffered(true);
x = y = 10;
timer = new Timer(25, this); // Создаем объект timer
        timer.start();      // стартует  timer    4
// Каждые 25 ms timer будет вызывать метод actionPerformed
}

public void paint(Graphics g) {      // 5  
super.paint(g);
Graphics2D g2d = (Graphics2D)g;
g2d.drawImage(star, x, y, this);
Toolkit.getDefaultToolkit().sync();
g.dispose();
}

public void actionPerformed(ActionEvent e) {   //  6 
x += 1;
y += 1;
if (y > 240) { 
y = -45;
x = -45;
}
repaint();
}
}

В классе Board используется интерфейс ActionListener, реализованный в подключаемом библиотечном классе javax.swing.Timer. С помощью интерфейса  к  объекту класса Board (источнику события)  подключается объект  «таймер» (слушатель события). Функция actionPerformed (обработчик события) вызывается через каждые 25 мс. Промежуток времени устанавливается при создании объекта timer.

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

Дополнительно к рассмотренной реализации класса Board ниже приводятся две альтернативные версии.

Board.java (2-я версия)

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;

import java.util.Timer;
import java.util.TimerTask;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
public class Board extends JPanel {
 Image star;
 Timer timer;
 int x, y;
 public Board() {
 setBackground(Color.BLACK);
ImageIcon ii = new ImageIcon(this.getClass().getResource("star.png"));
 star = ii.getImage();
 setDoubleBuffered(true);
 x = y = 10;
 timer = new Timer();
 timer.scheduleAtFixedRate(new ScheduleTask(), 100, 10);
 }
 public void paint(Graphics g) {
 super.paint(g);
 Graphics2D g2d = (Graphics2D)g;
 g2d.drawImage(star, x, y, this);
 Toolkit.getDefaultToolkit().sync();
 g.dispose();
 }
 class ScheduleTask extends TimerTask {
 public void run() {
 x += 1;
 y += 1;
 if (y > 240) {
 y = -45;
 x = -45;
 }
 repaint();
 }
 }
}
В этой версии используем библиотеку java.util.Timer вместо javax.Swing.Timer. При этом вместо интерфейса ActionListener для анимации используется объект класса ScheduleTask, производный от класса TimerTask. Таймер каждые 10 мс вызывает метод run () класса ScheduleTaskclass. Начальная задержка составляет 100 мс.

 

Board.java (3-я версия)

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
public class Board extends JPanel implements Runnable {
 private Image star;
 private Thread animator;
 private int x, y;
 private final int DELAY = 50;
 public Board() {      //  3
 setBackground(Color.BLACK);
 setDoubleBuffered(true);
 ImageIcon ii = new ImageIcon(this.getClass().getResource("star.png"));
 star = ii.getImage();
 x = y = 10;
 }
 public void addNotify() {   // 5
 super.addNotify();
 animator = new Thread(this);
 animator.start();
 }
 public void paint(Graphics g) {   // 4
 super.paint(g);
 Graphics2D g2d = (Graphics2D)g;
 g2d.drawImage(star, x, y, this);
 Toolkit.getDefaultToolkit().sync();
 g.dispose();
 }
 public void cycle() {
 x += 1;
 y += 1;
 if (y > 240) {
 y = -45;
 x = -45;
 }
 }
 public void run() {        // 6
 long beforeTime, timeDiff, sleep;
 beforeTime = System.currentTimeMillis();
 while (true) {
 cycle();
 repaint();
 timeDiff = System.currentTimeMillis() - beforeTime;
 sleep = DELAY - timeDiff;
 if (sleep < 0)
 sleep = 2;
 try {
 Thread.sleep(sleep);
 } catch (InterruptedException e) {
 System.out.println("interrupted");
 }
 beforeTime = System.currentTimeMillis();
 }
 }
}

Анимация объектов с помощью потока (Thread) — самый точный способ анимации. Он реализуется через интерфейс Runnable. В предыдущих примерах мы выполнили задачу через определенные промежутки времени. В этом примере анимация будет проходить внутри потока.

Метод addNotify () вызывается после того, как панель была добавлен в JFrame компонент — add(new Board()). Этот метод часто используется для различных задач инициализации.

Метод run () вызывается только один раз — при создании объекта animator.  Из этого метода в бесконечном цикле while вызываются методы cycle () и repaint (). Время выполнения этих методов может быть различным в каждом из while циклов. А мы хотим, чтобы анимация проходила на постоянной скорости. Поэтому вычисляем  разницу timeDiff системного времени до и после запуска обоих методов.  Эту разницу вычитаем из константы DELAY (50 мс), корректируя тем самым необходимую задержку (sleep) выполнения потока.

Ошибки, возникшие в программе во время её работы обрабатываются через исключения.  Обработка исключений произведена в программе с помощью операторов try…catch.

Игровое приложение ”Snake”

Snake (Змея) – одна из старейших классических видеоигр.  В этой игре  голова змеи перемещается с помощью клавиш управления курсором, хвост следует за ней.

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

Исходный код программы приводится ниже. Файлы рисунка «1.png»  и  «2.png»  размещается в директории, где находятся файлы классов проекта. Анимация реализуется через рассмотренный выше способ использования таймера (см.  Анимация изображения).

Snake.java

import javax.swing.JFrame;
public class Snake extends JFrame {
public Snake() {
add(new Board());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(320, 340);
setLocationRelativeTo(null);
setTitle("Snake");
setResizable(false);
setVisible(true);
}

public static void main(String[] args) {
new Snake();
}
}

Board.java

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Board extends JPanel implements ActionListener {
private final int WIDTH = 300;
private final int HEIGHT = 300;
private final int DOT_SIZE = 10;
private final int ALL_DOTS = 900;
private final int RAND_POS = 29;
private final int DELAY = 140;
private int x[] = new int[ALL_DOTS];
private int y[] = new int[ALL_DOTS];
private int dots;
private int apple_x;
private int apple_y;
private boolean left = false;
private boolean right = true;
private boolean up = false;
private boolean down = false;
private boolean inGame = true;
private Timer timer;
private Image ball;
private Image apple;
private Image head;

public Board() {
addKeyListener(new TAdapter());
setBackground(Color.black);
ImageIcon iid = new ImageIcon(this.getClass().getResource("1.png"));
ball = iid.getImage();
ImageIcon iia = new ImageIcon(this.getClass().getResource("1.png"));
apple = iia.getImage();
ImageIcon iih = new ImageIcon(this.getClass().getResource("2.png"));
head = iih.getImage();
setFocusable(true);
initGame();
}

public void initGame() {
dots = 3;
for (int z = 0; z < dots; z++) {
x[z] = 50 - z*10;
y[z] = 50;
}

locateApple();
timer = new Timer(DELAY, this);
        timer.start();
}

public void paint(Graphics g) {
super.paint(g);
if (inGame) {
g.drawImage(apple, apple_x, apple_y, this);
for (int z = 0; z < dots; z++) {
if (z == 0)
g.drawImage(head, x[z], y[z], this);
else g.drawImage(ball, x[z], y[z], this);
}
Toolkit.getDefaultToolkit().sync();
g.dispose();
} else {
gameOver(g);
}
}

public void gameOver(Graphics g) {
String msg = "Game Over";
Font small = new Font("Helvetica", Font.BOLD, 14);
FontMetrics metr = this.getFontMetrics(small);
g.setColor(Color.white);
g.setFont(small);
g.drawString(msg, (WIDTH - metr.stringWidth(msg)) / 2,
HEIGHT / 2);
}

public void checkApple() {
if ((x[0] == apple_x) && (y[0] == apple_y)) {
dots++;
locateApple();
}
}

public void move() {
for (int z = dots; z > 0; z--) {
x[z] = x[(z - 1)];
y[z] = y[(z - 1)];
}
if (left) {
x[0] -= DOT_SIZE;
}
if (right) {
x[0] += DOT_SIZE;
}
if (up) {
y[0] -= DOT_SIZE;
}
if (down) {
y[0] += DOT_SIZE;
}
}

public void checkCollision() {
for (int z = dots; z > 0; z--) {
if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
inGame = false;
}
}
if (y[0] > HEIGHT) {
inGame = false;
}
if (y[0] < 0) {
inGame = false;
}

if (x[0] > WIDTH) {
inGame = false;
}
if (x[0] < 0) {
inGame = false;
}
}

public void locateApple() {
int r = (int) (Math.random() * RAND_POS);
apple_x = ((r * DOT_SIZE));
r = (int) (Math.random() * RAND_POS);
apple_y = ((r * DOT_SIZE));
}

public void actionPerformed(ActionEvent e) {
if (inGame) {
checkApple();
checkCollision();
move();
}
repaint();
}

private class TAdapter extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if ((key == KeyEvent.VK_LEFT) && (!right)) {
left = true;
up = false;
down = false;
}
if ((key == KeyEvent.VK_RIGHT) && (!left)) {
right = true;
up = false;
down = false;
}

if ((key == KeyEvent.VK_UP) && (!down)) {
up = true;
right = false;
left = false;
}
if ((key == KeyEvent.VK_DOWN) && (!up)) {
down = true;
right = false;
left = false;
}
}
}
}

Контрольные задания

Ознакомиться с программой “Snake” и последовательно модифицировать ее:

  1. Автоматизировать работу программы, т.е. обеспечить движение змейки к яблоку, без вмешательства пользователя.
  2. Обеспечить передвижение яблока (жертвы) в точку (random — выбор).
  3. Обеспечить с помощью клавиш управления курсором передвижение яблока. При этом игра приобретает новый статус, где жертва (например, мышка) убегает от охотника.
  4. Обеспечить управление передвижением жертвы с помощью мышки.

Сетевые приложения

Краткий обзор сетевых приложений

Ниже, на 3-х сетевых Java приложениях  рассмотрено, как можно написать программы без знания всей глубины сетевых технологий (ресурсы операционной системы, маршрутизация между сетями, поиск адресов, физическая среда передачи и т.д.). Но все же вкратце рекомендуется ознакомиться с теоретическими основами разработки сетевых приложений на Java.

В приложениях используется клиент-серверная парадигма, которую примерно можно определить следующим образом:

  1. Одна из программ, называемая сервером, ожидает, пока программа-клиент подключится к ней.
  2. Клиент подключается.
  3. Сервер и клиент обмениваются информацией.
  4. Связь между клиентом и сервером закрывается.

Каждое из приложений демонстрирует решение определенной задачи:

–   Приложение “A Date Server and Client” обеспечивает простую одностороннюю связь. Сервер отправляет данные только одному подключенному клиенту.

–  Приложение “A capitalize server and client” демонстрирует двустороннюю связь сервера с множеством подключенных к нему клиентов.

–    Игра для двух игроков “Крестики-нолики”  показывает работу сервера, который должен следить за состоянием игры и информировать клиентов, чтобы каждый мог обновить свои данные в соответствии с изменениями у другого клиента. Т.е., сервер выступает в качестве посредника при общении 2-х клиентов между собой.

Приложение “Date Server and Client” с одним клиентом

Приложение состоит из 2-х программ, одна выполняется на стороне сервера, другая – на стороне клиента. При последовательном запуске программ в окне консоли появляется сообщение об активизации сервера и клиента.

Перед очередным запуском программ не забывайте закрывать программы, которые остались открытыми (running…) от предыдущих запусков (см. нижнюю строку окна «Output»).

При запуске программы-клиента также появляется окно “Input”. После ввода в текстовое окно IP-адреса сервера (localhost) появляется окно “Message” с данными от сервера (текущая дата и время).

Исходный код программы-сервера (файл DateServer.java):

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class DateServer {
public static void main(String[] args) throws IOException {
// создаем сокет сервера и привязываем его к порту 9090
ServerSocket listener = new ServerSocket(9090);
try {
while (true) {
Socket socket = listener.accept();
try {
PrintWriter out =
new PrintWriter(socket.getOutputStream(), true);
out.println(new Date().toString());
} finally {
socket.close();
}
break; // выход из цикла и переход к блоку finally
} // заканчивается бесконечный цикл
}
finally { // блок finally выполняется всегда
listener.close();
}
}
}

Программа-сервер постоянно находится в состоянии ожидания, она прослушивает (listen) сеть, ожидая запросов от клиентов. Программа содержит класс DateServer с единственным методом main. Причем, этот метод объявляется так, что он может выбросить  исключение (throws IOException).

В программе создаются сокеты. Сокет представляет собой программную конструкцию (объект), которая определяет конечную точку соединения. Вначале создается объект класса ServerSocket, затем — в бесконечном цикле ожидания while (true) объект класса Socket. Главное отличие ServerSocket от Socket, что объект первого класса (listener) заставляет программу ждать подключений. При подключении метод listener.accept() создает объект socket.

Затем создается объект out класса PrintWriter для вывода текста в поток. В параметрах конструктора  указывается направление потока socket.getOutputStream() (выходной поток сокета) и задается автоматический сброс буфера (параметр autoFlush = true). Метод out.println (“текст”) обеспечивает запись текста в поток.

В бесконечном цикле while (true) можно передавать данные множеству подключаемых клиентов, если закомментировать break. Однако, при этом не предусмотрено закрытие объекта listener, оно возможно лишь через диспетчер задач.

Исходный код программы-клиента  (файл DateClient.java):

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import javax.swing.JOptionPane;

public class DateClient {
    public static void main(String[] args) throws IOException {
        String serverAddress = JOptionPane.showInputDialog(
            "Enter IP Address of a machine that is\n" +
            "running the date service on port 9090:");
        Socket s = new Socket(serverAddress, 9090);
        BufferedReader input =
 new BufferedReader(new InputStreamReader(s.getInputStream()));
// Строка из буфера считывается в переменную
        String answer = input.readLine();
  // Строка отображается в диалоговом окне
        JOptionPane.showMessageDialog(null, answer);
        s.close();
        System.exit(0); // завершение процесса
    }
}

Вначале запускается dialog box с предложением ввести IP address сервера, затем клиент присоединяется к серверу (создается сокет s)  и тот передает ему дату и время, которые отображаются в диалоговом окне.

Для получения данных от сервера открывается входной поток s.getInputStream(). А далее цепочка читателей: InputStreamReader читает байты из потока и преобразует их в символы; BufferedReader объединяет символы в строку. Строка отображается в диалоговом окне.

Приложение “Capitalization Server and Client” с несколькими клиентами

Это приложение, как и предыдущее, состоит из 2-х программ, одна выполняется на стороне сервера, другая – на стороне клиента. При последовательном запуске программ в окне консоли появляется сообщение об активизации сервера и клиента.

При запуске программы-клиента появляются окно “Capitalize Client”  и окно “Welcome to the Capitalization Program” с текстовым окном для ввода IP-адреса сервера. После ввода IP-адреса сервера в окне “Capitalize Client” клиенту предлагается ввести строку . После ввода текста и нажатия клавиши Enter сервер получает строку, преобразует маленькие буквы в большие и возвращает обновленную строку клиенту.

Сервер позволяет подключаться нескольким клиентам.

Когда один из клиентов посылает строку, содержащую точку «.», программа прекращает цикл и закрывается.

Исходный код программы-сервера и программы-клиента приводится ниже.

Программа-сервер (файл CapitalizeServer.java):

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/*
 * Программа-сервер принимает запросы от клиентов 
 * преобразовать буквы строки на заглавные
 * Когда клиент подключается, он посылает серверу строку
 * и получает обратно версию строки заглавными буквами.
 */
public class CapitalizeServer {

/*
 * При соединении socket (поток) передается на обслуживание 
 * а serversocket возвращается к прослушиванию очередного клиента.
 * Server хранит номер номер каждого клиента,
 * чтобы обрабатывать сообщения каждого потока
 */
 public static void main(String[] args) throws Exception {
 System.out.println("The capitalization server is running.");
 int clientNumber = 0;
 ServerSocket serversocket = new ServerSocket(9898);
 // выражения должны быть помещены в блок try-finally (обработка исключений),
 // чтобы при каких-либо ошибках произошла очистка памяти.
 // Позаботится об этом необходимо, потому что сокеты используют важные ресурсы,
 // не относящиеся к памяти, так что вы должны очищать их 
 //(в Java нет деструкторов, чтобы сделать это за вас).
 try {
 while (true) {
 // Метод start() класса thread вызывает метод run().
 // Если метод run() вызывается напрямую,
 // его код выполняется с текущим а не новым потоком.
 new Capitalizer(serversocket.accept(), clientNumber++).start();
 }
 } finally {
 serversocket.close();
 // System.exit(0);
 }
 }

/**
 * A private thread to handle capitalization requests
 * on a particular socket. 
 * The client terminates the dialogue by sending
 * a single line containing only a period.
 */
 // Класс Thread с интерфейсом Runnable (запускается run при создании объекта)
 // Thread. В этом классе определены все методы, необходимые для создания потоков,
 // При этом в рамках вашего класса необходимо определить метод run.
 // Он получает управление при запуске потока методом start.
 private static class Capitalizer extends Thread {
 private Socket socket;
 private int clientNumber;

public Capitalizer(Socket socket, int clientNumber) {
 this.socket = socket;
 this.clientNumber = clientNumber;
 log("New connection with client# " + clientNumber + " at " + socket);
 }
 
 public void run() {
 try { 
 BufferedReader in = new BufferedReader(
 new InputStreamReader(socket.getInputStream()));
 PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
 // Send a welcome message to the client.
 out.println("Hello, you are client #" + clientNumber + ".");
 out.println("Enter a line with only a period to quit\n");
 while (true) {
 String input = in.readLine();
 if (input == null || input.equals(".")) {
 break;
 //System.exit(0);
 }
 out.println(input.toUpperCase());
 }
 } catch (IOException e) {
 log("Error handling client# " + clientNumber + ": " + e);
 } finally {
 try {
 socket.close();
 System.exit(0);
 } catch (IOException e) {
 log("Couldn't close a socket, what's going on?");
 }
 log("Connection with client# " + clientNumber + " closed");
 }
 }

/**
 * Logs a simple message. In this case we just write 
 * the message to the server applications standard output.
 */
 private void log(String message) {
 System.out.println(message);
 }
 }
}

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

В классе Capitalizer (производном от Thread) с интерфейсом Runnable определены все методы, необходимые для создания потоков. В рамках класса необходимо определить метод run. Он получает управление при запуске потока методом start.

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

Программа-клиент (файл CapitalizeClient.java):

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
 * A simple Swing-based client for the capitalization server.
 * 
 * При запуске CapitalizeClient открывается окно
 * с текстовым полем для ввода строки
 * и текстовым полем для вывода обработанной сервером строки
 */
public class CapitalizeClient {

private BufferedReader in;
 private PrintWriter out;
 private JFrame frame = new JFrame("Capitalize Client");
 private JTextField dataField = new JTextField(40);
 private JTextArea messageArea = new JTextArea(8, 60);

/**
 * Constructs the client by laying out the GUI
 * and registering a listener with the textfield
 * so that pressing Enter in the listener
 * sends the textfield contents to the server.
 */
 public CapitalizeClient() {

// Layout GUI
 messageArea.setEditable(false);
 frame.getContentPane().add(dataField, "North");
 frame.getContentPane().add(new JScrollPane(messageArea), "Center");

// Add Listeners
 dataField.addActionListener(new ActionListener() {
 /**
 * Responds to pressing the enter key
 * in the textfield
 * by sending the contents of the text field
 * to the server
 * and displaying the response from the server
 * in the text area.
 * If the response is "." we exit
 * the whole application, which closes all sockets,
 * streams and windows.
 */
 public void actionPerformed(ActionEvent e) {
 out.println(dataField.getText());
 String response;
 try {
 response = in.readLine();
 if (response == null || response.equals("")) {
 System.exit(0);
 }
 } catch (IOException ex) {
 response = "Error: " + ex;
 }
 messageArea.append(response + "\n");
 dataField.selectAll();
 }
 });
 }

/**
 * Implements the connection logic by prompting
 * the end user for the server's IP address,
 * connecting, setting up streams, 
 * and consuming the welcome messages from the server.
 * The Capitalizer protocol says
 * that the server sends three lines of text to the client
 * immediately after establishing a connection.
 */
 public void connectToServer() throws IOException {

// Get the server address from a dialog box.
 String serverAddress = JOptionPane.showInputDialog(
 frame,
 "Enter IP Address of the Server:",
 "Welcome to the Capitalization Program",
 JOptionPane.QUESTION_MESSAGE);

// Make connection and initialize streams
 Socket socket = new Socket(serverAddress, 9898);
 in = new BufferedReader(
 new InputStreamReader(socket.getInputStream()));
 out = new PrintWriter(socket.getOutputStream(), true);

// Consume the initial welcoming messages from the server
 for (int i = 0; i < 3; i++) {
 messageArea.append(in.readLine() + "\n");
 }
 }

/**
 * Runs the client application.
 */
 public static void main(String[] args) throws Exception {
 CapitalizeClient client = new CapitalizeClient();
 client.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 client.frame.pack();
 client.frame.setVisible(true);
 client.connectToServer();
 }
}

Игра для двух игроков “Крестики-нолики”

Приложение “Tic Tac Toe game” (игра “Крестики-нолики”)  состоит из 2-х программ, одна выполняется на стороне сервера, другая – на стороне клиента. При последовательном запуске программ в окне консоли появляются сообщения об активизации сервера и клиента.

При запуске программы-клиента также появляется окно “Player X”, при  повторном ее запуске – окно “ Player O”. Дальнейшее развитие и окончание игры видно из рисунка.

Исходный код программы-сервера и программы-клиента приводится ниже. Рисунки  и размещается в директории, где находятся файлы классов проекта программы-клиента.

Программа-сервер (файл TicTacToeServer.java):

 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.net.ServerSocket;
 import java.net.Socket;

public class TicTacToeServer {
 public static void main(String[] args) throws Exception {
 ServerSocket listener = new ServerSocket(8901);
 System.out.println("Tic Tac Toe Server is Running");
 try {
 while (true) {
 Game game = new Game();
 Game.Player playerX = game.new Player(listener.accept(), 'X');
 Game.Player playerO = game.new Player(listener.accept(), 'O');
 playerX.setOpponent(playerO);
 playerO.setOpponent(playerX);
 game.currentPlayer = playerX;
 playerX.start();
 playerO.start();
 }
 } finally {
 listener.close();
 }
 }
 }

class Game {
 private Player[] board = {
 null, null, null,
 null, null, null,
 null, null, null};

Player currentPlayer;
 public boolean hasWinner() {
 return
 (board[0] != null && board[0] == board[1] && board[0] == board[2])
 ||(board[3] != null && board[3] == board[4] && board[3] == board[5])
 ||(board[6] != null && board[6] == board[7] && board[6] == board[8])
 ||(board[0] != null && board[0] == board[3] && board[0] == board[6])
 ||(board[1] != null && board[1] == board[4] && board[1] == board[7])
 ||(board[2] != null && board[2] == board[5] && board[2] == board[8])
 ||(board[0] != null && board[0] == board[4] && board[0] == board[8])
 ||(board[2] != null && board[2] == board[4] && board[2] == board[6]);
 }
 public boolean boardFilledUp() {
 for (int i = 0; i < board.length; i++) {
 if (board[i] == null) {
 return false;
 }
 }
 return true;
 }

public synchronized boolean legalMove(int location, Player player) {
 if (player == currentPlayer && board[location] == null) {
 board[location] = currentPlayer;
 currentPlayer = currentPlayer.opponent;
 currentPlayer.otherPlayerMoved(location);
 return true;
 }
 return false;
 }
 class Player extends Thread {
 char mark;
 Player opponent;
 Socket socket;
 BufferedReader input;
 PrintWriter output;
 public Player(Socket socket, char mark) {
 this.socket = socket;
 this.mark = mark;
 try {
 input = new BufferedReader(
 new InputStreamReader(socket.getInputStream()));
 output = new PrintWriter(socket.getOutputStream(), true);
 output.println("WELCOME " + mark);
 output.println("MESSAGE Waiting for opponent to connect");
 } catch (IOException e) {
 System.out.println("Player died: " + e);
 }
 }
 public void setOpponent(Player opponent) {
 this.opponent = opponent;
 }
 public void otherPlayerMoved(int location) {
 output.println("OPPONENT_MOVED " + location);
 output.println(
 hasWinner() ? "DEFEAT" : boardFilledUp() ? "TIE" : "");
 }
 public void run() {
 try {
 output.println("MESSAGE All players connected");
 if (mark == 'X') {
 output.println("MESSAGE Your move");
 } while (true) {
 String command = input.readLine();
 if (command.startsWith("MOVE")) {
 int location = Integer.parseInt(command.substring(5));
 if (legalMove(location, this)) {
 output.println("VALID_MOVE");
 output.println(hasWinner() ? "VICTORY"
 : boardFilledUp() ? "TIE" : "");
 } else {
 output.println("MESSAGE ?");
 }
 } else if (command.startsWith("QUIT")) {
 return;
 }
 }
 } catch (IOException e) {
 System.out.println("Player died: " + e);
 } finally {
 try {socket.close();} catch (IOException e) {}
 }
 }
 }
 }

В функции main программы-сервера (файл TicTacToeServer.java) создается объект listener и запускается бесконечный цикл.

В начале цикла создается объект класса Game. В этом классе описаны данные и  методы, которые позволяют следить за состоянием игры и информировать клиентов, чтобы каждый мог обновить свои данные в соответствии с изменениями у другого клиента. В классе Game также описан встроенный класс Player, производный от класса Thread.

Далее в цикле объект listener прослушивает и подключает 2-х игроков-клиентов. Каждый из игроков (player) передается на обслуживание побочных потоков, а в конструкторе создаются сокет, потоки ввода-вывода и клиентам передаются приветствие и метка (mark) – X или O. Метка служит для идентификации клиента.

Далее потоки переходят в состояние “ Running” запуском (через start) метода run(), который определен в классе Player. В методе run() клиенту с меткой X передается сообщение начать игру («MESSAGE Your move»).

Затем организован собственный бесконечный цикл внутри потока. Здесь происходит обработка данных. От клиента (текущего игрока) поступает номер указанного мышкой квадратика, он сохраняется в переменной location. Методы класса Game по значению этой переменной и наполненности игровой доски определяют текущее состояние игры. Наполненность доски фиксируется в массиве board ссылками на игроков, которые заполнили соответствующие квадраты. Заполняется доска в методе legalMove. В соответствии с текущим состоянием игры передаются сообщения клиентам.

Метод legalMove определен с ключевым словом synchronized. Такой метод запрещает нескольким потокам одновременный доступ к нему. Прежде, чем начать выполнять его, поток пытается заблокировать объект, у которого вызывается метод. При завершении исполнения (как успешном, так и в случае ошибок) производится операция unlock, чтобы освободить объект для других потоков.

Метод legalMove вызывается в потоке игрока, который пытается сделать ход и проверяет, или ход является правильным. То есть, игрок выполняющий ход, должен быть текущим игроком и квадрат, в котором он пытается сделать ход, не должен быть уже занятым. Если ход правильный, состояние игры обновляется (квадрат заполнен, следующий игрок становится текущим, и он уведомляется о своем ходе).

Программа-клиент (файл TicTacToeClient.java):

import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class TicTacToeClient {
private JFrame frame = new JFrame("Tic Tac Toe");
private JLabel messageLabel = new JLabel("");
private ImageIcon icon;
private ImageIcon opponentIcon;
private Square[] board = new Square[9];
private Square currentSquare;
private static int PORT = 8901;
private Socket socket;
private BufferedReader in;
private PrintWriter out;

public TicTacToeClient(String serverAddress) throws Exception {
// Setup networking
socket = new Socket(serverAddress, PORT);
in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
// Layout GUI
messageLabel.setBackground(Color.lightGray);
frame.getContentPane().add(messageLabel, "South");
JPanel boardPanel = new JPanel();
boardPanel.setBackground(Color.black);
boardPanel.setLayout(new GridLayout(3, 3, 2, 2));
for (int i = 0; i < board.length; i++) {
final int j = i;
board[i] = new Square();
board[i].addMouseListener(new MouseAdapter() {

public void mousePressed(MouseEvent e) {
currentSquare = board[j];
out.println("MOVE " + j);
//System.out.println(j);
}});
boardPanel.add(board[i]);
}
frame.getContentPane().add(boardPanel, "Center");
}

public void play() throws Exception {
String response;
try {
response = in.readLine();
if (response.startsWith("WELCOME")) {
char mark = response.charAt(8);
if (mark == 'X') {
icon = new ImageIcon(getClass().getResource("x.gif"));
opponentIcon = new ImageIcon(getClass().getResource("o.gif"));
}else {
icon = new ImageIcon(getClass().getResource("o.gif"));
opponentIcon = new ImageIcon(getClass().getResource("x.gif"));
}
frame.setTitle("Player " + mark);
}
while (true) {
response = in.readLine();
if (response.startsWith("VALID_MOVE")) {
messageLabel.setText("Valid move, please wait");
currentSquare.setIcon(icon);
currentSquare.repaint();
}
else if (response.startsWith("OPPONENT_MOVED")) {
int loc = Integer.parseInt(response.substring(15));
board[loc].setIcon(opponentIcon);
board[loc].repaint();
messageLabel.setText("Opponent moved, your turn");
} else if (response.startsWith("VICTORY")) {
messageLabel.setText("You win");
break;
} else if (response.startsWith("DEFEAT")) {
messageLabel.setText("You lose");
break;
} else if (response.startsWith("TIE")) {
messageLabel.setText("You tied");
break;
} else if (response.startsWith("MESSAGE")) {
messageLabel.setText(response.substring(8));
}
}
out.println("QUIT");
}
finally {
socket.close();
}
}

private boolean wantsToPlayAgain() {
int response = JOptionPane.showConfirmDialog(frame,
"Want to play again?",
"Tic Tac Toe is Fun Fun Fun",
JOptionPane.YES_NO_OPTION);
frame.dispose();
return response == JOptionPane.YES_OPTION;
}
static class Square extends JPanel {
JLabel label = new JLabel((Icon)null);
public Square() {
setBackground(Color.white);
add(label);
}

public void setIcon(Icon icon) {
label.setIcon(icon);
}
}

public static void main(String[] args) throws Exception {
while (true) {
String serverAddress = (args.length == 0) ? "localhost" : args[1];
TicTacToeClient client = new TicTacToeClient(serverAddress);
client.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
client.frame.setSize(240, 160);
client.frame.setVisible(true);
client.frame.setResizable(false);
client.play();
if (!client.wantsToPlayAgain()) {
break;
}
}
}
}

В функции main программы-клиента запускается бесконечный цикл. В нем создается объект client класса TicTacToeClient. При этом конструктор устанавливает связь с сервером, создает сокет, потоки ввода-вывода, панель с массивом квадратных ячеек board[i]. Объекту каждой ячейки добавляется событие mousePressed, при котором через поток вывода серверу передается номер выбранной ячейки.

При вызове метода client.play() от сервера поступает сообщение о начале игры и метка (X или O), присвоенная игроку. Затем создается внутренний бесконечный цикл. Здесь происходит обработка сообщений сервера, полученных в ответ на выполненный ход (указание мышкой квадратика).

Контрольное задание

На основе игрового приложения ”Snake” создать клиент-серверное приложение для 2-х игроков, где первый управляет движением змейки, а второй – движением жертвы. Победителем считается первый игрок, если он настигает жертву за отведенное время игры (определяется таймером), в противном случае побеждает второй игрок.

С программным кодом выполнения этого задания  можете ознакомиться  по ссылке (Snake_net). Разработал приложение студент специальности «Компьютерные науки и информационные технологии»  Лаврентий Антон.

Полезные ссылки

Основы Java

Введение в язык программирования java

Что такое ООП на примерах. Для чайников

Вопросы и ответы на собеседование Java Junior Developer

Java — Потоки — Stream и Thread

 

 

Автор: Николай Свирневский

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *