Русский
Русский
English
Статистика
Реклама

Ввод-вывод

Перевод Приёмы и хитрости начинающего Java-программиста

05.10.2020 10:14:40 | Автор: admin

Небольшая коллекция практик, трюков и подсказок, с помощью которых вы сэкономите своё время при изучении Java и написании кода на этом языке программирования. Перевод статьиTop 25 Java Tricks, Tips, and Best Practices от специалистов Рексофт.

Организация работы

Чистый код

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

  • Правило 10-50-500. В одном пакете не может быть более 10 классов. Каждый метод должен быть короче 50 строк кода, а каждый класс короче 500 строк.

  • SOLID принципы.

  • Использование паттернов проектирования.

Работа с ошибками

Stack Trace (Трассировка стека)

Выявление ошибок это, пожалуй, самая трудоемкая часть процесса разработки на Java. Трассировка стека позволяет вам точно отслеживать, где именно в проекте возникла ошибка или исключение (exception).

import java.io.*;Exception e = ;java.io.StringWriter sw = new java.io.StringWriter();e.printStackTrace(new java.io.PrintWriter(sw));String trace = sw.getBuffer().toString();

NullPointerException

Исключения, возникающие из-заnullзначений (NullPointerException), довольно часто появляются при попытке вызвать метод у несуществующего объекта.

Возьмем для примера следующий код:

int noOfStudents = school.listStudents().count;private int getListOfStudents(File[] files) {if (files == null)  throw new NullPointerException("File list cannot be null");}

Примечание переводчика: а вот пример от меня как переводчика материала:

String anyString = null;if (anyString.equals("some string")) { // здесь будет null pointer exception, т.к. anyString == null// ...}

Дата и Время

System.currentTimeMillis или System.nanoTime?

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

МетодSystem.currentTimeMillis()возвращает текущее количество миллисекунд с начала эры Unix в формате Long. Его точность составляет от 1 до 15 тысячных долей секунды в зависимости от системы.

long startTime = System.currentTimeMillis();long estimatedTime = System.currentTimeMillis() - startTime;

МетодSystem.nanoTime()имеет точность до одной миллионной секунды (наносекунды) и возвращает текущее значение наиболее точного доступного системного таймера.

long startTime = System.nanoTime();long estimatedTime = System.nanoTime() - startTime;

Таким образом, методSystem.currentTimeMillis()лучше применять для отображения и синхронизации абсолютного времени, аSystem.nanoTime()для измерения относительных интервалов времени.

Валидация Даты из строки

Если необходимо достать объектDateиз обычной строки в Java, можете использовать небольшой утилитный класс, который приведен ниже. Он позаботится обо всех сложностях валидации и преобразовании строки в объектDate.

package net.viralpatel.java;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;public class DateUtil {// List of all date formats that we want to parse.// Add your own format here.private static List; dateFormats = new ArrayList() {{add(new SimpleDateFormat("M/dd/yyyy"));add(new SimpleDateFormat("dd.M.yyyy"));add(new SimpleDateFormat("M/dd/yyyy hh:mm:ss a"));add(new SimpleDateFormat("dd.M.yyyy hh:mm:ss a"));add(new SimpleDateFormat("dd.MMM.yyyy"));add(new SimpleDateFormat("dd-MMM-yyyy"));}};/** * Convert String with various formats into java.util.Date *  * @param input *            Date as a string * @return java.util.Date object if input string is parsed  * successfully else returns null */public static Date convertToDate(String input) {Date date = null;if(null == input) {return null;}for (SimpleDateFormat format : dateFormats) {try {format.setLenient(false);date = format.parse(input);} catch (ParseException e) {//Shhh.. try other formats}if (date != null) {break;}}return date;}}

Пример его использования:

package net.viralpatel.java;public class TestDateUtil {public static void main(String[] args) {System.out.println("10/14/2012" + " = " + DateUtil.convertToDate("10/14/2012"));System.out.println("10-Jan-2012" + " = " + DateUtil.convertToDate("10-Jan-2012"));System.out.println("01.03.2002" + " = " + DateUtil.convertToDate("01.03.2002"));System.out.println("12/03/2010" + " = " + DateUtil.convertToDate("12/03/2010"));System.out.println("19.Feb.2011" + " = " + DateUtil.convertToDate("19.Feb.2011" ));System.out.println("4/20/2012" + " = " + DateUtil.convertToDate("4/20/2012"));System.out.println("some string" + " = " + DateUtil.convertToDate("some string"));System.out.println("123456" + " = " + DateUtil.convertToDate("123456"));System.out.println("null" + " = " + DateUtil.convertToDate(null));}}

Результат:

10/14/2012 = Sun Oct 14 00:00:00 CEST 201210-Jan-2012 = Tue Jan 10 00:00:00 CET 201201.03.2002 = Fri Mar 01 00:00:00 CET 200212/03/2010 = Fri Dec 03 00:00:00 CET 201019.Feb.2011 = Sat Feb 19 00:00:00 CET 20114/20/2012 = Fri Apr 20 00:00:00 CEST 2012some string = null123456 = nullnull = null

Строки

Оптимизация строки

Для конкатенации (сложения) строк в Java используется оператор +, для примера, в циклеforновый объект может создаваться для каждой новой строки, что приводит к потере памяти и увеличению времени работы программы.

Необходимо избегать создания Java строк через конструктор, пример:

// медленное создание строкиString bad = new String ("Yet another string object");// быстрое создание строкиString good = "Yet another string object"

Одинарные и двойные кавычки

Что ты ожидаешь в результате выполнения этого кода?

public class Haha {  public static void main(String args[]) {    System.out.print("H" + "a");    System.out.print('H' + 'a');  }}

Казалось бы, строка должна возвращать HaHa, но на самом деле это будет Ha169.

Двойные кавычки обрабатывают символы как строки, но одинарные кавычки ведут себя иначе. Они преобразуют символьные операнды ('H'и'a') в целые значения посредством расширения примитивных типов получается 169.

Математика

Float или Double?

Программисты часто не могут выбрать необходимую точность для чисел с плавающей запятой. Float требует всего 4 байта, но имеет только 7 значащих цифр, а Double в два раза точнее (15 цифр), но в два раза прожорливее.

Фактически, большинство процессоров могут одинаково эффективно работать как с Float, так и с Double, поэтому воспользуйтесь рекомендацией Бьорна Страуструпа (автор языка С++):

Выбор правильной точности для решения реальных задач требует хорошего понимания природы машинных вычислений. Если у вас его нет, либо посоветуйтесь с кем-нибудь, либо изучите проблему самостоятельно, либо используйте Double и надейтесь на лучшее.

Проверка на нечетность

Можно ли использовать этот код для точного определения нечетного числа?

public boolean oddOrNot(int num) {  return num % 2 == 1;}

Надеюсь, вы заметили хитрость. Если мы решим таким образом проверить отрицательное нечетное число (например, -5), остаток от деления не будет равен единице, поэтому воспользуйтесь более точным методом:

public boolean oddOrNot(int num) {  return (num & 1) != 0;}

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

Возведение в степень

Возвести число в степень можно двумя способами:

  1. простое умножение;

  2. используя методMath.pow()(двойное основание, двойной показатель степени).

Использование библиотечной функции рекомендуется только в случае крайней необходимости, например, в случае дробной или отрицательной степени.

double result = Math.pow(625, 0.5); // 25.0

Простое умножение в Java работает в 300-600 раз эффективнее, кроме того, его можно дополнительно оптимизировать:

double square = double a * double a;  double cube = double a * double a * double a; // не оптимизированоdouble cube = double a * double square;  // оптимизировано double quad = double a * double a * double a * double a; // не оптимизированоdouble quad = double square * double square; // оптимизировано

JIT оптимизация

Код Java обрабатывается с использованием JIT-компиляции: сначала он транслируется в платформенно-независимый байт-код, а затем в машинный код. При этом оптимизируется все возможное, и разработчик может помочь компилятору создать максимально эффективную программу.

В качестве примера рассмотрим две простые операции:

// 1n += 2 * i * i; // 2n += 2 * (i * i);

Давайте измерим время выполнения каждого из них:

// 1long startTime1 = System.nanoTime(); int n1 = 0; for (int i = 0; i < 1000000000; i++) {   n1 += 2 * i * i; } double resultTime1 = (double)(System.nanoTime() - startTime1) / 1000000000;System.out.println(resultTime1 + " s");  // 2long startTime2 = System.nanoTime(); int n2 = 0; for (int i = 0; i < 1000000000; i++) {   n2 += 2 * (i * i); } double resultTime2 = (double)(System.nanoTime() - startTime2) / 1000000000;System.out.println(resultTime2 + " s");

Запустив этот код несколько раз, мы получим примерно следующее:

|`2*(i*i)`| `2*i*i`||---------|--------||0.5183738 | 0.6246434||0.5298337 | 0.6049722||0.5308647 | 0.6603363||0.5133458 | 0.6243328||0.5003011 | 0.6541802||0.5366181 | 0.6312638||0.515149  | 0.6241105||0.5237389 | 0.627815 ||0.5249942 | 0.6114252||0.5641624 | 0.6781033||0.538412  | 0.6393969||0.5466744 | 0.6608845||0.531159  | 0.6201077||0.5048032 | 0.6511559||0.5232789 | 0.6544526|

Схема очевидна: группировка переменных в круглые скобки ускоряет работу программы. Это связано с генерацией более эффективного байт-кода при умножении одинаковых значений.

Вы можете узнать больше об этом экспериментездесь. Или можете провести свой собственный тест, используя онлайн-компилятор Java.

Структуры данных

Комбинирование хеш-таблиц

Комбинирование двух хеш-таблиц вручную через цикл очень неэффективно. Вот альтернативное решение этой проблемы, которое вам возможно понравится:

import java.util.*;Map m1 = ;Map m2 = ;m2.putAll(m1); // добавить к m2 все элементы из m1

Array или ArrayList?

Выбор междуArrayиArrayListзависит от специфики задачи Java, которую вы хотите решить. Запомните следующие особенности этих типов:

  • Массив имеет фиксированный размер, и память для него выделяется во время объявления, а размерArrayListможет динамически меняться.

  • Массивы Java работают намного быстрее, а вArrayListнамного проще добавлять и удалять элементы.

  • При работе сArrayскорее всего возникнет ошибкаArrayIndexOutOfBoundsException.

  • ArrayList может быть только одномерным, когда массивы Java могут быть многомерными.

import java.util.*; public class ArrayVsArrayList {  public static void main(String[] args) {    // создаем массив    int[] myArray = new int[6];     // ссылаемся на несуществующий индекс    myArray[7]= 10; // ArrayIndexOutOfBoundsException     // создаем ArrayList    List myArrayList = new ArrayList<>();     // простое добавление и удаление элементов    myArrayList.add(1);    myArrayList.add(2);    myArrayList.add(3);    myArrayList.add(4);    myArrayList.add(5);    myArrayList.remove(0);     // перебор элементов ArrayList     for(int i = 0; i < myArrayList.size(); i++) {      System.out.println("Element: " + myArrayList.get(i));    }     //  многомерный массив    int[][][] multiArray = new int[3][3][3];   }}

JSON

Сериализация и Десериализация

JSON невероятно удобный и полезный синтаксис для хранения и обмена данными. Java полностью поддерживает это.

Примечание переводчика: для использования JSON из примера необходимо подключить библиотекуJSON Simple.

Вы можете сериализовать данные следующим образом:

import org.json.simple.JSONObject;import org.json.simple.JSONArray; public class JsonEncodeDemo {  public static void main(String[] args) {    JSONObject obj = new JSONObject();    obj.put("Novel Name", "Godaan");    obj.put("Author", "Munshi Premchand");     JSONArray novelDetails = new JSONArray();    novelDetails.add("Language: Hindi");    novelDetails.add("Year of Publication: 1936");    novelDetails.add("Publisher: Lokmanya Press");            obj.put("Novel Details", novelDetails);      System.out.print(obj);    }}

Получается следующая строка JSON:

{"Novel Name":"Godaan","Novel Details":["Language: Hindi","Year of Publication: 1936","Publisher: Lokmanya Press"],"Author":"Munshi Premchand"}

Десериализация в Java выглядит так:

import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.util.Iterator; import org.json.simple.JSONArray;import org.json.simple.JSONObject;import org.json.simple.parser.JSONParser;import org.json.simple.parser.ParseException; public class JsonParseTest {  private static final String filePath = "//home//user//Documents//jsonDemoFile.json";      public static void main(String[] args) {    try {      // читаем json файл      FileReader reader = new FileReader(filePath);      JSONParser jsonParser = new JSONParser();      JSONObject jsonObject = (JSONObject)jsonParser.parse(reader);                  // берем данные из json объекта      Long id =  (Long) jsonObject.get("id");      System.out.println("The id is: " + id);                  String type = (String) jsonObject.get("type");      System.out.println("The type is: " + type);       String name = (String) jsonObject.get("name");      System.out.println("The name is: " + name);       Double ppu =  (Double) jsonObject.get("ppu");      System.out.println("The PPU is: " + ppu);       // извлекаем массив              System.out.println("Batters:");      JSONArray batterArray= (JSONArray) jsonObject.get("batters");      Iterator i = batterArray.iterator();            // проходимся по всем элементам массива      while (i.hasNext()) {        JSONObject innerObj = (JSONObject) i.next();        System.out.println("ID "+ innerObj.get("id") +             " type " + innerObj.get("type"));      }       System.out.println("Topping:");      JSONArray toppingArray= (JSONArray) jsonObject.get("topping");      Iterator j = toppingArray.iterator();      while (j.hasNext()) {        JSONObject innerObj = (JSONObject) j.next();        System.out.println("ID "+ innerObj.get("id") +             " type " + innerObj.get("type"));      }    } catch (FileNotFoundException|IOException|ParseException|NullPointerException ex) {      ex.printStackTrace();    }  }}

Используемый в примере файл JSON (jsonDemoFile.json):

{  "id": 0001,  "type": "donut",  "name": "Cake",  "ppu": 0.55,  "batters":    [      { "id": 1001, "type": "Regular" },      { "id": 1002, "type": "Chocolate" },      { "id": 1003, "type": "Blueberry" },      { "id": 1004, "type": "Devil's Food" }    ],  "topping":    [      { "id": 5001, "type": "None" },      { "id": 5002, "type": "Glazed" },      { "id": 5005, "type": "Sugar" },      { "id": 5007, "type": "Powdered Sugar" },      { "id": 5006, "type": "Chocolate with Sprinkles" },      { "id": 5003, "type": "Chocolate" },      { "id": 5004, "type": "Maple" }    ]}

Примечание переводчика: в Java проектах очень часто для работы с JSON используют библиотеки Gson от Google или Jackson. Обе библиотеки очень популярны и хорошо поддерживаются. Попробуйте и их.

Ввод и Вывод

FileOutputStream или FileWriter?

Запись файлов в Java осуществляется двумя способами:FileOutputStreamиFileWriter. Какой метод выбрать, зависит от конкретной задачи.

FileOutputStreamпредназначен для записи необработанных байтовых потоков. Это делает его идеальным решением, например, для работы с изображениями.

File foutput = new File(file_location_string);FileOutputStream fos = new FileOutputStream(foutput);BufferedWriter output = new BufferedWriter(new OutputStreamWriter(fos));output.write("Buffered Content");

УFileWriterдругое призвание: работа с потоками символов. Поэтому, если вы пишете текстовые файлы, выберите этот метод.

FileWriter fstream = new FileWriter(file_location_string);BufferedWriter output = new BufferedWriter(fstream);output.write("Buffered Content");

Производительность и лучшие практики

Пустая коллекция вместо Null

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

import java.util.*public List getLocations() {  List result = someService.getLocationsFromDB();  return result == null ? Collections.emptyList() : result;}

Создание объектов только когда необходимо

Создание объектов одна из самых затратных операций в Java. Лучше всего создавать их только тогда, когда они действительно нужны.

import java.util.ArrayList;import java.util.List; public class Employees {  private List Employees;   public List getEmployees() {    // initialization only if necessary    if(null == Employees) {      Employees = new ArrayList();    }        return Employees;  }}

Deadlocks (Дедлоки)

Взаимная блокировка (Deadlock) потоков может происходить по многим причинам, и полностью защититься от них в Java 8 очень сложно. Чаще всего, это происходит, когда один синхронизируемый объект ожидает ресурсов, которые заблокированы другим синхронизированным объектом.

Вот пример тупика этого потока:

public class DeadlockDemo {  public static Object addLock = new Object();  public static Object subLock = new Object();   public static void main(String args[]) {    MyAdditionThread add = new MyAdditionThread();    MySubtractionThread sub = new MySubtractionThread();    add.start();    sub.start();  }   private static class MyAdditionThread extends Thread {    public void run() {      synchronized (addLock) {        int a = 10, b = 3;        int c = a + b;        System.out.println("Addition Thread: " + c);        System.out.println("Holding First Lock...");        try { Thread.sleep(10); }        catch (InterruptedException e) {}        System.out.println("Addition Thread: Waiting for AddLock...");        synchronized (subLock) {           System.out.println("Threads: Holding Add and Sub Locks...");        }      }    }  }   private static class MySubtractionThread extends Thread {    public void run() {      synchronized (subLock) {        int a = 10, b = 3;        int c = a - b;        System.out.println("Subtraction Thread: " + c);        System.out.println("Holding Second Lock...");        try { Thread.sleep(10); }        catch (InterruptedException e) {}        System.out.println("Subtraction  Thread: Waiting for SubLock...");        synchronized (addLock) {           System.out.println("Threads: Holding Add and Sub Locks...");        }      }    }  }}

Результат этой программы:

=====Addition Thread: 13Subtraction Thread: 7Holding First Lock...Holding Second Lock...Addition Thread: Waiting for AddLock...Subtraction  Thread: Waiting for SubLock...

Взаимоблокировок можно избежать, изменив порядок вызова потоков:

private static class MySubtractionThread extends Thread {  public void run() {    synchronized (addLock) {      int a = 10, b = 3;      int c = a - b;      System.out.println("Subtraction Thread: " + c);      System.out.println("Holding Second Lock...");      try { Thread.sleep(10); }      catch (InterruptedException e) {}      System.out.println("Subtraction  Thread: Waiting for SubLock...");      synchronized (subLock) {        System.out.println("Threads: Holding Add and Sub Locks...");      }    }  }}

Вывод:

=====Addition Thread: 13Holding First Lock...Addition Thread: Waiting for AddLock...Threads: Holding Add and Sub Locks...Subtraction Thread: 7Holding Second Lock...Subtraction  Thread: Waiting for SubLock...Threads: Holding Add and Sub Locks...

Резервирование памяти

Некоторые приложения Java очень ресурсоемки и могут работать медленно. Для повышения производительности вы можете выделить на машине Java больше оперативной памяти.

export JAVA_OPTS="$JAVA_OPTS -Xms5000m -Xmx6000m -XX:PermSize=1024m -XX:MaxPermSize=2048m"
  • Xms минимальный пул выделения памяти;

  • Xmx максимальный пул выделения памяти;

  • XX: PermSize начальный размер, который будет выделен при запуске JVM;

  • XX: MaxPermSize максимальный размер, который можно выделить при запуске JVM.

Примечание переводчика: PermSize и MaxPermSize, начиная с Java 8 уже больше не используются.

Решение распространенных проблем

Содержимое директории

Java позволяет вам получать имена всех подкаталогов и файлов в папке в виде массива, который затем можно последовательно прочитать:

import java.io.*; public class ListContents {  public static void main(String[] args) {    File file = new File("//home//user//Documents/");    String[] files = file.list();     System.out.println("Listing contents of " + file.getPath());    for(int i=0 ; i < files.length ; i++) {      System.out.println(files[i]);    }  }}

Выполнение консольных команд

Java позволяет выполнять консольные команды прямо из кода, используя классRuntime. Очень важно не забывать об обработке исключений.

Например, давайте попробуем открыть файл PDF через терминал Java (на Linuxe):

public class ShellCommandExec {  public static void main(String[] args) {    String gnomeOpenCommand = "gnome-open //home//user//Documents//MyDoc.pdf";     try {      Runtime rt = Runtime.getRuntime();      Process processObj = rt.exec(gnomeOpenCommand);       InputStream stdin = processObj.getErrorStream();      InputStreamReader isr = new InputStreamReader(stdin);      BufferedReader br = new BufferedReader(isr);       String myoutput = "";       while ((myoutput=br.readLine()) != null) {        myoutput = myoutput+"\n";      }      System.out.println(myoutput);    } catch (Exception e) {      e.printStackTrace();    }  }

Воспроизведение звуков

Звук важный компонент многих десктопных приложений и игр. Язык программирования Java предоставляет средства для работы с ним.

import java.io.*;import java.net.URL;import javax.sound.sampled.*;import javax.swing.*; public class PlaySoundDemo extends JFrame {    // constructor   public playSoundDemo() {      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);      this.setTitle("Play Sound Demo");      this.setSize(300, 200);      this.setVisible(true);       try {         URL url = this.getClass().getResource("MyAudio.wav");         AudioInputStream audioIn = AudioSystem.getAudioInputStream(url);         Clip clip = AudioSystem.getClip();         clip.open(audioIn);         clip.start();      } catch (UnsupportedAudioFileException|IOException|LineUnavailableException e) {         e.printStackTrace();      }   }    public static void main(String[] args) {      new PlaySoundDemo();   }}

Отправка email

Отправить электронную почту на Java очень просто. Вам просто нужно установитьJava Mailи указать путь к нему в пути к классам проекта.

import java.util.*;import javax.mail.*;import javax.mail.internet.*; public class SendEmail {  public static void main(String [] args) {        String to = "recipient@gmail.com";    String from = "sender@gmail.com";    String host = "localhost";     Properties properties = System.getProperties();    properties.setProperty("mail.smtp.host", host);    Session session = Session.getDefaultInstance(properties);     try{      MimeMessage message = new MimeMessage(session);      message.setFrom(new InternetAddress(from));       message.addRecipient(Message.RecipientType.TO,new InternetAddress(to));       message.setSubject("My Email Subject");      message.setText("My Message Body");      Transport.send(message);      System.out.println("Sent successfully!");    } catch (MessagingException ex) {      ex.printStackTrace();    }  }}

Получение координат курсора

Чтобы фиксировать события мыши, вам необходимо реализовать интерфейсMouseMotionListener. Когда курсор попадает в определенную область, срабатывает обработчик событияmouseMoved, из которого вы можете получить точные координаты (используя Swing для UI)

import java.awt.event.*;import javax.swing.*; public class MouseCaptureDemo extends JFrame implements MouseMotionListener {  public JLabel mouseHoverStatus;   public static void main(String[] args) {    new MouseCaptureDemo();  }   MouseCaptureDemo() {    setSize(500, 500);    setTitle("Frame displaying Coordinates of Mouse Motion");     mouseHoverStatus = new JLabel("No Mouse Hover Detected.", JLabel.CENTER);    add(mouseHoverStatus);    addMouseMotionListener(this);    setVisible(true);  }   public void mouseMoved(MouseEvent e) {    mouseHoverStatus.setText("Mouse Cursor Coordinates => X:"+e.getX()+" | Y:"+e.getY());  }   public void mouseDragged(MouseEvent e) {  }}
Подробнее..

О реализации ввода-вывода с именами

05.01.2021 22:19:51 | Автор: admin

Введение

Как-то на одном из компьютерных форумов участники делились соображениями, чего им не хватает в языках, так сказать, в повседневной деятельности. Один заявил, что ему очень не хватает оператора типа put data. Для тех, кто не слышал о таком, поясню, что в языке PL/1 был предусмотрен оператор вывода (или ввода) значений переменных вместе с их именами, т.е. вместе с идентификаторами.

Например, если обычный вывод put list(X,Y); давал что-нибудь вроде:

1280 1024

то оператор put data(X,Y); печатал:

X= 1280 Y= 1024

не заставляя писать для этого вывод в виде оператора:

put list(X=,X,Y=,Y);

Чаще всего это использовалось в отладочных целях, но не обязательно. Например, используя вывод в текстовый файл с помощью put data, а затем ввод этих же данных как исходных в другой программе с помощью зеркального оператора get data можно было получить удобную для чтения и редактирования форму представления исходных данных в текстовом виде.

Можно возразить, что современные оболочки систем программирования легко позволяют вывести значения (и имена) любых объектов программы в интерактивном режиме безо всяких put data и поэтому в нем больше нет необходимости.

Я тоже использую встроенный интерактивный отладчик, с помощью которого можно посмотреть любые, даже сложные (составные) данные. Однако потребность в возможностях put data не исчезла. Иногда нужен и анализ данных на распечатке, (т.е. в логе) с возможностью быстрого и удобного добавления в текст программы вывода для отладки. К тому же для работы отладочных интерактивных систем используется дополнительная информация, которая обычно в выполняемом exe-файле отсутствует. В этом же случае все имена переменных зашиты в самой программе. Таким образом, удобства, предоставляемые языком в виде оператора, подобного put data, не заменяют, а дополняют возможности отладчиков. Кстати, в стародавние времена так называемая посмертная выдача на печать после ошибки иногда включала в себя и вывод полного списка всех имен переменных вместе с их текущими значениями. Некоторым программистам так нравился такой вывод, что они специально в конце своей программы ставили деление на ноль.

Реализации ввода-вывода с именами

В исходном варианте компилятора, которым я пользуюсь и который по мере сил развиваю, оператора put data не было. И мне его тоже очень не хватало в процессе отладки. Требовалось его ввести, причем хотелось сделать все как можно проще, компактнее и не внося больших изменений в компилятор. Поэтому была принята следующая схема реализации:

1. Сначала компилятором выполняется обычный разбор очередного объекта, перечисленного в списке ввода-вывода. Но при этом все лексемы, которые выдал лексический анализатор по запросу компилятора, еще и запоминаются в специальном буфере. По окончании разбора результат (т.е. все получившиеся операции внутреннего представления программы) отбрасывается, но характеристики объекта, например, его имя в программе, размерность и т.п., становятся известны.

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

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

Для реализации put data достаточно проанализировать запомненные в буфере лексемы, найти конец разбираемого элемента (это запятая вне скобок или внешняя закрывающая скобка) и все предыдущие лексемы объединить в одну строку, которую в начале работы эмулятора и выдать с признаком текстовая константа.

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

Таким образом, например, текст:

put data(x(i+1,j-1));

внутри компилятора будет восприниматься как:

put list(x(i+1,j-1)=,x(i+1,j-1));

Разумеется, так будут обрабатываться не только объекты-переменные, но и любые выражения при выводе. Исключение составляет лишь вывод текстовых констант. Для них дополнительные лексемы, конечно же, не формируются во избежание бессмысленного вывода одного и того же текста два раза подряд со знаком равенства посередине.

Ввод-вывод не скалярных объектов

Идея исправлять исходный текст программы на лету позволила легко решить поставленную задачу. Однако полученный механизм жаль было использовать только для такой мелочи как put data, поскольку этот же подход можно было применить и для более объемной задачи ввода-вывода не скалярных объектов. Автоматический ввод-вывод всех элементов не скалярного объекта также был предусмотрен в языке и тоже отсутствовал в исходной версии компилятора, хотя на практике часто требовался.

Здесь ситуация сложнее, чем с put data для скаляра, так как для вывода массива вместо текста:

put data(x);

требовалось на лету сформировать уже что-нибудь вроде:

do i=lbound(x) to hbound(x); put data(x(i)); end;

Но и это еще не все. Например, если x двумерный массив целых (например, матрица 3х3), тот же самый оператор put data(x); уже должен осуществлять вывод по двум циклам, чтобы дать в результате что-то такое:

X(1,1)= 1 X(1,2)= 2 X(1,3)= 3 X(2,1)= 4 X(2,2)= 5 X(2,3)= 6 X(3,1)= 7 X(3,2)= 8 X(3,3)= 9

Но при этом оператор put data(x(2)); должен вывести лишь одну строку матрицы:

X(2,1)= 4 X(2,2)= 5 X(2,3)= 6

А если x это набор разнотипных элементов (в PL/1 такой набор называется структурой), то нужно еще выводить и имена отдельных полей этого набора, например:

declare

1 a,

2 a1 fixed,

2 a2 fixed,

2 b,

3 b1 fixed,

3 b2 float;

put data(a);

A.A1= 0 A.A2= 0 A.B.B1= 0 A.B.B2= 0.000000E+00

Учитывая, что в структуре могут быть подструктуры, в свою очередь включающие массивы (т.е. могут быть и структуры массивов и массивы структур), то задача автоматического вывода элементов становится довольно громоздкой:

declare

1 a(1:2),

2 a1 fixed,

2 a2 fixed,

2 b,

3 b1 (-5:3) fixed,

3 b2 float;

put data(b);

B(1).B1(-5)= 0 B(1).B1(-4)= 0 B(1).B1(-3)= 0 B(1).B1(-2)= 0 B(1).B1(-1)= 0 B(1).B1(0)= 0 B(1).B1(1)= 0 B(1).B1(2)= 0 B(1).B1(3)= 0 B(1).B2= 0.000000E+00 B(2).B1(-5)= 0 B(2).B1(-4)= 0 B(2).B1(-3)= 0 B(2).B1(-2)= 0 B(2).B1(-1)= 0 B(2).B1(0)= 0 B(2).B1(1)= 0 B(2).B1(2)= 0 B(2).B1(3)= 0 B(2).B2= 0.000000E+00

Однако в языке PL/1 есть хорошая основа для доработок в виде возможности записать оператор цикла прямо внутри оператора ввода-вывода. В свое время разработчикам языка вывод массивов казался очень важным. Поэтому они разрешили писать цикл ввода-вывода в виде:

put list((x(i) do i=lbound(x) to hbound(x)));

вместо обычного цикла:

do i=lbound(x) to hbound(x); put list(x(i)); end;

Признаком начала цикла внутри оператора ввода-вывода является открывающая скобка. Кстати, разработчики PL/1 здесь немного попали впросак, поскольку при выводе выражения, начинающегося со скобки, большинство компиляторов воспримет эту скобку за начало цикла. Программистам приходилось писать выражения в экзотическом виде, только чтобы они не начинались со скобки:

put list(1e0*(x1+x2)/2e0));

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

Легко заметить, что вывод с именами (put data) и вывод не скалярных объектов это дополняющие друг друга возможности. Объединяет их то, что в обоих случаях разбор объекта при трансляции выполняется два раза. Сначала все лексемы при лексическом разборе объекта запоминаются, а при повторном разборе выдаются из буфера так, как если бы из исходного текста. Но при этом в нужный момент выдаются и новые лексемы. В обоих случаях вместо лексического анализатора работает его эмулятор. Когда нужны обе возможности одновременно работают оба эмулятора, один на более низком уровне выдает лексемы для индексов, названий полей структур и заголовков циклов, а второй добавляет к ним элементы с текстовыми константами-именами.

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

При этом работа эмулятора нижнего уровня гораздо сложнее, чем в случае put data, здесь используется такой алгоритм, как конечный автомат. Т.е. эмулятор имеет несколько состояний, запоминаемых между вызовами, при разборе переходя от одного к другому и выдавая в каждом состоянии лексемы разного типа. Это позволяет сначала выдать одну или несколько лексем - открывающих скобок, затем все имена промежуточных уровней структуры и все индексы, как переменные циклов, а затем набор лексем, обозначающих заголовки циклов по каждой размерности и, наконец, лексемы - закрывающие скобки.

Естественно, что если в исходном тексте старшие индексы объекта были явно указаны, то эмулятор дописывает только недостающие младшие индексы. Поэтому, например, для трехмерного массива x(1:10,1:10,1:10) оператор вывода:

put list(x); выдаст 1000 значений

put list(x(5)); выдаст 100 значений

put list(x(5,6)); выдаст 10 значений.

А для оператора put list(x(5,6,8)); эмулятор вообще не сгенерирует дополнительных лексем-индексов и будет напечатано единственное значение.

Недостатком описанной реализации оказалось то, что при применении оператора put data к не скалярным объектам на печати вместо всех значений индексов печатались идентификаторы переменных циклов, которые подставлялись в исходный текст в момент трансляции. А поскольку для этой цели использовались служебные целые переменные ?A, ?B, ?С,?O (всего 15 штук для максимально возможной размерности массива 15) выдача выглядела странно и во многом теряла ценность:

X(?A,?B)= 1 X(?A,?B)= 2 X(?A,?B)= 3 X(?A,?B)= 4 X(?A,?B)= 5 X(?A,?B)= 6 X(?A,?B)= 7 X(?A,?B)= 8 X(?A,?B)= 9

Для исправления этого недостатка потребовалось доработать системную библиотеку. В случае оператора put data с индексами компилятор не просто формирует текстовую константу имя переменной с подставленными индексами ?A, ?B, ?С,?O, но и дописывает перед каждым индексом специальный не выводимый символ 07FH.

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

Переменные с именами при вводе

Естественно, имеется парный оператор к put data - это get data, который должен вводить значения переменных, заданные вместе с их именами. Но здесь, на мой взгляд, разработчики PL/1 переусложнили данный оператор. В соответствии со стандартом языка оператор ввода должен не просто пропускать имена, но распознавать их и записывать следующее далее значение по нужному адресу. Например, если перетасовать значения, выданные put data(x);, в другом порядке:

X(1,2)= 2 X(1,1)= 1 X(2,3)= 6 X(2,1)= 4 X(2,2)= 5 X(3,1)= 7 X(1,3)= 3 X(3,2)= 8 X(3,3)= 9

То оператор get data(x); все равно должен ввести значения в правильном порядке, распознав каждое имя и индексы перед знаком равенства. И хотя такая обработка дает дополнительные удобства, например, можно пропускать данные, которые не меняются при вводе, все-таки это слишком хлопотно и сложно для реализации. А, главное, для такой возможности нужно иметь таблицу имен переменных и их адресов в каждом exe-файле, использующем get data. Поэтому я не стал реализовывать распознавание имен при вводе, а ограничился при чтении простым пропуском всех символов до знака равенства при разборе очередного элемента. Самое важное для меня операторы put data и get data остались зеркальными и выдача данных оператором put позволяет потом безошибочно читать их оператором get с таким же списком объектов.

Заключение

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

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

Послесловие

По личному опыту знаю, что, самая распространенная реакция на подобные заметки обычно сводится к двум заявлениям:

1. Это нафиг никому не нужно.

2.Это легко повторить на любом современном языке.

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

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

Поэтому мне была предложена несложная библиотека на Немерле, реализующая, как уверял ее автор, вывод с именами и повторяющий возможности старого PL/1:

using System.Console;using PL1.Macro;module Program{  Main() : void  {    def x = 1280;    def y = 1024;    put data(x, y);    put data(x, y, x + y, x.ToString() + "asd");    _ = ReadLine();  }}using Nemerle;using Nemerle.Compiler;using Nemerle.Compiler.Parsetree;using System.Collections.Generic;using System.Linq;namespace PL1.Macro{  public macro PutData(args)  syntax ("put", "data", args)  {    PutDataImpl.Impl(args)  }  module PutDataImpl  {    public Impl(args : PExpr) : PExpr    {      match (args)      {        | PExpr.Tuple as args =>          def code = List();          foreach (arg in args.args)          {            code.Add(<[ $(arg.ToString()) ]>);            code.Add(<[ "=" ]>);            code.Add(arg);            code.Add(<[ " " ]>);          }          code.RemoveAt(code.Count - 1);          def code = code.Aggregate((a1, a2) => <[ $a1 + $a2 ]>);          <[ System.Console.WriteLine($(code)) ]>        | _ => PExpr.Error(args.Location, "Tuple expected.")      }    }  }}

Однако на предложение доработать библиотеку, чтобы она позволяла выводить и не скалярные объекты с печатью всех индексов (и тогда действительно бы повторяла возможности PL/1), мне в ответ было предложено сделать это самому в виде домашнего задания. Оказанную честь я отклонил, под предлогом того, что вообще-то у меня все это уже давно есть и используется. Судя по всему, автор библиотеки просто сразу не сообразил, как это сделать. И почему-то вспомнилась сцена из фильма Формула любви, где Калиостро удивив всех поеданием вилки, все-таки отказался съесть при этом еще и тарелку.

Подробнее..

Recovery mode МОБА здорового человека (концепт)

24.12.2020 22:23:19 | Автор: admin
На здоровье сегодняшнего киберспортсмена сказывается целый ряд негативных факторов, источником которых является несовершенство спортинвентаря (устройств ввода). В связи с этим принято решение, в преддверии скорого выхода основной статьи на тему, опубликовать в качестве иллюстрации к ней один из концептов трансформации устройств ввода-вывода для игр в изометрической проекции.

Просим прощения за качество контента. Концепт родился за пару минут на пике дебатов в отсутствии иллюстратора.

image

Сенсорный экран выводящий разом всё игровое поле + вывод основных внутриигровых команд на замыкаемые в различных комбинациях контакты на руку/руки.

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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru