1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-thinking-in-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
17.1 文字处理.md 78 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 11.03.2025 09:15 d56454c

17.1 Обработка текста

Если у вас есть опыт работы с C или C++, то вы можете начать сомневаться в способностях Java контролировать текст. На самом деле, мы больше всего боялись медленной скорости выполнения, что могло бы ограничить наши возможности. Однако, инструменты Java (особенно класс String) обладают мощными возможностями, как показывают примеры этой главы (и производительность также значительно улучшена).

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

17.1.1 Выделение списков кодаДля каждого полного списка кода в книге (не отдельных фрагментов) вы наверняка обратили внимание на специальные метки начала и конца (````//:и///:~``` ). Эти метки используются для автоматического извлечения кода из книги в совместимые файлы исходного кода. В моей предыдущей книге я разработал систему, которая автоматически объединяет проверенные файлы кода в текст книги. Однако для этой книги я решил использовать более простой подход — после первоначальной проверки просто скопировать код внутрь книги. Поскольку сложно сразу добиться компиляции, я редактировал код внутри книги. Но как извлекать и тестировать этот код? Вот где важна эта программа. Если вам требуется решить задачу обработки текста, она будет очень полезна. Этот пример также демонстрирует множество свойств класса String.Сначала я сохраняю всю книгу в виде одного файла ASCII-текста. Программа `CodePackager` имеет два режима выполнения (описание которых содержится в строке `usageString`): если используется флаг `-p`, программа проверяет входной файл, содержащий ASCII-текст (то есть содержимое книги). Она проходит через этот файл, извлекает код согласно меткам и использует имя файла первой строки для создания имени выходного файла. Кроме того, когда требуется поместить файл в специальное расположение, программа проверяет директиву `package` (в соответствии со значением пути, указанного директивой `package`). Но этого недостаточно. Программа также должна отслеживать имя пакета (`package`), чтобы следить за изменениями внутри главы. Поскольку все используемые в каждой главе пакеты начинаются с `c02`, `c03`, `c04` и так далее, что указывает на принадлежность к конкретной главе (за исключением тех, которые начинаются с `com`, они игнорируются при отслеживании разных глав) — достаточно того, что первый список кода в каждой главе содержит `package`, поэтому программа `CodePackager` может знать о изменениях в каждой главе и помещает последующие файлы в новые подкаталоги. Каждый выделенный файл помещается в объект `SourceCodeFile`, после чего этот объект добавляется в коллекцию (подробнее об этом процессе будет рассказано позже).Эти объекты `SourceCodeFile` могут быть просто сохранены в файлах, что является второй задачей данного проекта. При прямом вызове `CodePackager` без использования флага `-p` он принимает «упакованный» файл в качестве входных данных. Этот файл затем распаковывается (выгружается) в отдельные файлы. Таким образом, флаг `-p` указывает на то, что файлы были «упакованы» (packed).Но зачем вообще использовать упакованные файлы? Это связано с тем, что различные компьютерные платформы хранят текстовые данные в файлах по-разному. Основной проблемой являются методы представления символов новой строки; конечно, существуют и другие проблемы. Однако Java имеет специальный тип потока ввода/вывода — `DataOutputStream` — который гарантирует, что «независимо от того, какое устройство было использовано для создания данных, они могут быть правильно сохранены в правильном формате устройства при помощи `DataInputStream`». То есть, Java берёт на себя контроль над всеми деталями, связанными с различными платформами, что делает его особенно мощным. Поэтому флаг `-p` позволяет сохранять все данные в одном файле в универсальном формате. Пользователи могут скачать этот файл и программу Java с веб-сайта, а затем запустить `CodePackager` для этого файла без указания флага `-p`. В результате файлы будут выгружены в правильное место системы (или можно указать другой подкаталог; если это не сделано, подкаталог создаётся в текущем каталоге). Чтобы гарантировать, что форматы, зависящие от конкретной платформы, не остаются, мы используем объект `File` всякий раз, когда требуется описать файл или путь. Кроме того, существует особая мера безопасности: в каждом подкаталоге создается пустой файл, имя которого указывает количество файлов, которое должно находиться в данном подкаталоге.Вот полный код, который будет подробно объяснён далее:

//: CodePackager.java
// Упаковывает и распаковывает код из "Thinking in Java"
// для распределения между платформами.
/* Комментарий, чтобы CodePackager видел его и начинал новый каталог главы,
   но чтобы вам не пришлось беспокоиться о каталоге, где находится эта программа:
package c17;
*/
import java.util.*;
import java.io.*;
``````java
class Pr {
  static void error(String e) {
    System.err.println("ОШИБКА: " + e);
    System.exit(1);
  }
}
class ВводВыход {
  static BufferedReader открытьВвод(File f) {
    BufferedReader в = null;
    try {
      в = new BufferedReader(
        new FileReader(f));
    } catch(IOException e) {
      Pr.error("не удалось открыть " + f);
    }
    return в;
  }

  static BufferedReader открытьВвод(String имяФайла) {
    return открытьВвод(new File(имяФайла));
  }

  static DataOutputStream открытьЗапись(File f) {
    DataOutputStream в = null;
    try {
      в = new DataOutputStream(
        new BufferedOutputStream(
          new FileOutputStream(f)));
    } catch(IOException e) {
      Pr.error("не удалось открыть " + f);
    }
    return в;
  }

  static DataOutputStream открытьЗапись(String имяФайла) {
    return открытьЗапись(new File(имяФайла));
  }

  static PrintWriter открытьПечать(File f) {
    PrintWriter в = null;
    try {
      в = new PrintWriter(
        new BufferedWriter(
          new FileWriter(f)));
    } catch(IOException e) {
      Pr.error("не удалось открыть " + f);
    }
    return в;
  }

  static PrintWriter открытьПечать(String имяФайла) {
    return открытьПечать(new File(имяФайла));
  }

  static void закрыть(Writer os) {
    try {
      os.close();
    } catch(IOException e) {
      Pr.error("закрытие " + os);
    }
  }

  static void закрыть(DataOutputStream os) {
    try {
      os.close();
    } catch(IOException e) {
      Pr.error("закрытие " + os);
    }
  }

  static void закрыть(Reader os) {
    try {
      os.close();
    } catch(IOException e) {
      Pr.error("закрытие " + os);
    }
  }
}
# Класс SourceCodeFile
``````java
class SourceCodeFile {
    public static final String
      startMarker = "//:", // начало файла исходного кода
      endMarker = "} ///:~", // конец файла
      endMarker2 = "}; ///:~", // конец файла для C++
      beginContinue = "} ///:Continued",
      endContinue = "///:Continuing",
      packMarker = "###", // тэг начала архивированного файла
      eol = // разделитель строки в текущей системе
        System.getProperty("line.separator"),
      filesep = // разделитель пути файлов системы
        System.getProperty("file.separator");
    public static String copyright = "";
    static {
      try {
        BufferedReader cr =
          new BufferedReader(new FileReader("Copyright.txt"));
        String crin;
        while ((crin = cr.readLine()) != null)
          copyright += crin + "\n";
        cr.close();
      } catch (Exception e) {
        copyright = "";
      }
    }
    private String filename, dirname,
      contents = new String();
    private static String chapter = "c02";
    // разделитель имени файла от старой системы:
    public static String oldsep;
    public String toString() {
      return dirname + filesep + filename;
    }
    // конструктор для парсинга из файла документации:
    public SourceCodeFile(String firstLine,
        BufferedReader in) {
      dirname = chapter;
      // пропустить метку начала:
      filename = firstLine.substring(startMarker.length()).trim();
      // найти пробел, который завершает имя файла:
      if (filename.indexOf(' ') != -1)
        filename = filename.substring(0, filename.indexOf(' '));
      System.out.println("найдено: " + filename);
      contents = firstLine + eol;
      if (copyright.length() != 0)
        contents += copyright + eol;
      String s;
      boolean foundEndMarker = false;
      try {
        while ((s = in.readLine()) != null) {
          if (s.startsWith(startMarker))
            Pr.error("нет маркера конца файла для " + filename);
          // для этой программы, пробелы перед ключевым словом "package"
         // в исходном коде не допускаются:
          else if (s.startsWith("package")) {
            // Извлечь имя пакета:
            String pdir = s.substring(s.indexOf(' ')).trim();
            pdir = pdir.substring(0, pdir.indexOf(';')).trim();
            // Захватить главу из пакета, игнорируя подпапки 'com':
            if (!pdir.startsWith("com")) {
              int firstDot = pdir.indexOf('.');
              если (firstDot != -1) {
                  глава = pdir.substring(0, firstDot);
              } else {
                  глава = pdir;
              }
            }
            // Преобразование имени пакета в имя пути:
            pdir = pdir.replace('.', filesep.charAt(0));
            System.out.println("пакет " + pdir);
            dirname = pdir;
          }
          содержимое += s + eol;
          // Переходим за продолжением:
          если (s.startsWith(beginContinue)) {
            while ((s = in.readLine()) != null) {
              если (s.startsWith(endContinue)) {
                содержимое += s + eol;
                break;
              }
            }
          }
          // Обнаруживаем конец списка кода:
          если (s.startsWith(endMarker) || s.startsWith(endMarker2)) {
            найдено_конечное_маркер = true;
            break;
          }
        }
        если (! найдено_конечное_маркер) {
          Pr.error("Конечный маркер не найден до конца файла");
        }
        System.out.println("Глава: " + глава);
      } catch (IOException e) {
        Pr.error("Ошибка чтения строки");
      }
    }
    // Для восстановления из сжатого файла:
    public SourceCodeFile(BufferedReader pFile) {
      try {
        String s = pFile.readLine();
        если (s == null) return;
        если (! s.startsWith(packMarker)) {
          Pr.error("Невозможно найти " + packMarker + " в " + s);
        }
        s = s.substring(packMarker.length()).trim();
        dirname = s.substring(0, s.indexOf('#'));
        filename = s.substring(s.indexOf('#') + 1).trim();
      } catch (IOException e) {
        Pr.error("Ошибка чтения строки");
      }
    }

Здесь исправлены все необходимые части текста, чтобы они соответствовали правилам перевода на русский язык.```markdown substring(s.indexOf('#') + 1); dirname = dirname.replace(oldsep.charAt(0), filesep.charAt(0)); filename = filename.replace(oldsep.charAt(0), filesep.charAt(0)); System.out.println("список: " + dirname + filesep + filename); while ((s = pFile.readLine()) != null) { // Обнаруживаем конец списка кода: if (s.startsWith(endMarker) || s.startsWith(endMarker2)) { содержимое += s; break; } содержимое += s + eol; } } catch (IOException e) { System.err.println("Ошибка чтения строки"); } }

public boolean имеетФайл() { return filename != null; }

public String директория() { return dirname; }

public String имяФайла() { return filename; }

public String содержимое() { return contents; }

// Чтобы записать в сжатый файл: public void записатьСжатый(DataOutputStream out) { try { out.writeBytes(dirname + filesep + filename + "\n"); out.writeBytes(contents); } catch (IOException e) { System.err.println("Ошибка записи файла"); } }

```markdown
packMarker + dirname + "#"
+ filename + eol);
out.writeBytes(contents);
} catch (IOException e) {
Pr.error("writing " + dirname +
filesep + filename);
}
}
// Для создания фактического файла:
public void writeFile(String rootpath) {
File path = new File(rootpath, dirname);
path.mkdirs();
PrintWriter p = IO.psOpen(new File(path, filename));
p.print(contents);
IO.close(p);
}

Пожалуйста, обратите внимание, что в соответствии с правилами перевода, все имена переменных, функций, классов и команды CLI остались без изменений. Также сохранены пути к файлам и спецификация формата для Markdown.```markdown class DirMap { private Hashtable t = new Hashtable(); private String rootpath;

DirMap() { rootpath = System.getProperty("user.dir"); }

DirMap(String alternateDir) { rootpath = alternateDir; }

public void add(SourceCodeFile f) { String path = f.directory(); if (!t.containsKey(path)) { t.put(path, new Vector()); } ((Vector) t.get(path)).addElement(f); }

  public void writePackedFile(String fname) {
    DataOutputStream packed = IO.dosOpen(fname);
    try {
      packed.writeBytes("###Старый разделитель:" +
        SourceCodeFile.filesep + "###\n");
    } catch (IOException e) {
      Pr.error("Ошибка записи разделителя в " + fname);
    }
    Enumeration e = t.keys();
    while (e.hasMoreElements()) {
      String dir = (String) e.nextElement();
      System.out.println(
        "Запись директории " + dir);
      Vector v = (Vector) t.get(dir);
      for (int i = 0; i < v.size(); i++) {
        SourceCodeFile f =
            (SourceCodeFile) v.elementAt(i);
        f.writePacked(packed);
      }
    }
    IO.close(packed);
  }
```

Корректировки:

1. "Запись разделителя в" заменено на "Ошибка записи разделителя в".
2. Сохранены все остальные части кода без изменений.  // Запись всех файлов в их директориях:
  public void write() {
    Enumeration e = t.keys();
    while (e.hasMoreElements()) {
      String dir = (String) e.nextElement();
      Vector v = (Vector) t.get(dir);
      for (int i = 0; i < v.size(); i++) {
        SourceCodeFile f =
            (SourceCodeFile) v.elementAt(i);
        f.writeFile(rootpath);
      }
      // Добавление файла, указывающего количество
      // записанных файлов в эту директорию как проверки:
      IO.close(IO.dosOpen(
              new File(new File(rootpath, dir),
                       Integer.toString(v.size()) + ".files")));
    }
  }
}```markdown
публичный класс CodePackager {
    приватная статическая константа строка usageString =
        "использование: java CodePackager packedFileName" +
        "\nИзвлекает исходные коды файлов из упакованной версии Tjava.doc источников в директорию текущего каталога\n" +
        "java CodePackager packedFileName newDir\n" +
        "Извлекает в директорию newDir\n" +
        "java CodePackager -p source.txt packedFile\n" +
        "Создает упакованную версию исходных файлов из текстовой версии Tjava.doc";
    приватная статическая процедура usage() {
        System.err.println(usageString);
        System.exit(1);
    }

    публичная статическая процедура main(строка[] args) {
        если (args.length == 0) usage();
        если (args[0].равняется("-p")) {
            если (args.length != 3)
                usage();
            создать_u_пакованный_файл(args);
        } иначе {
            если (args.length > 2)
                usage();
            извлечь_u_пакованный_файл(args);
        }
    }

    приватная статическая строка currentLine;
    приватная статическая BufferedReader in;
    приватная статическая DirMap dm;

    приватная статическая процедура создать_u_пакованный_файл(строка[] args) {
        dm = новый DirMap();
        in = IO.disOpen(args[1]);
        попробовать {
            пока ((currentLine = in.readLine()) != null) {
                если (currentLine.начинается_c(SourceCodeFile.startMarker)) {
                    dm.добавить(новый SourceCodeFile(currentLine, in));
                } иначе если (currentLine.начинается_c(SourceCodeFile.
```
```markdown
```java
class CodePackager {
  public static void main(String[] args) {
    DirMap dm = new DirMap();
    try {
      InputStream in = IO.disOpen(args[0]);
      if (!in.markSupported())
        Pr.error("Файл не имеет начального маркера");
      else {
        in.mark(Integer.MAX_VALUE);
        String endMarker = in.readUTF();
        if (!endMarker.equals("END"))
          Pr.error("Файл не имеет начального маркера");
        // Иначе игнорировать входящую строку
      }
    } catch (IOException e) {
      Pr.error("Ошибка чтения " + args[1]);
    }
    IO.close(in);
    dm.writePackedFile(args[2]);
  }

  private static procedure extract_u_packed_file(String[] args) {
    if (args.length == 2) // Альтернативный каталог
      dm = new DirMap(args[1]);
    else // Текущий каталог
      dm = new DirMap();
    InputStream in = IO.disOpen(args[0]);
    String s = null;
    try {
      s = in.readLine();
    } catch (IOException e) {
      Pr.error("Невозможно прочитать из " + in);
    }
    // Захватывает разделитель, используемый в системе,
    // которая упаковала файл:
    if (s.indexOf("###Старый разделитель:") != -1) {
      String старый_разделитель = s.substring(
        "###Старый разделитель:".length());
      старый_разделитель = старый_разделитель.substring(
        0, старый_разделитель.indexOf('#'));
      SourceCodeFile.oldsep = старый_разделитель;
    }
    SourceCodeFile sf = new SourceCodeFile(in);
    while (sf.hasFile()) {
      dm.add(sf);
      sf = new SourceCodeFile(in);
    }
    dm.write();
  }
}
```

///:~Обратите внимание, что `package` директива уже выведена как закомментированная строка. Поскольку это первый пример в главе, `package` директива необходима для того, чтобы сообщить `CodePackager`, что мы сменили главу. Однако помещение её в пакет приведёт к проблемам. При создании пакета нам требуется связать конечную программу с определённой структурой каталогов, что применимо к большинству примеров этой книги.
```
```Но здесь `CodePackager` программа должна компилироваться и запускаться в отдельном специальном каталоге, поэтому `package` директива выводится как закомментированная строка. Тем не менее, для `CodePackager` она "представляет" собой обычную `package` директиву, поскольку программа ещё не достаточно сложна, чтобы распознавать многострочные комментарии (необходимость усложнять отсутствует, здесь требуется удобство).Первые два класса являются "поддержкой/инструментами", они делают остальную часть программы более последовательной и легче читаемой. Первый  это `Pr`, который аналогичен ANSI C библиотеке `perror`, которая может печатать сообщение об ошибке (хотя при этом завершает выполнение программы). Второй класс упаковывает процесс создания файла, этот процесс был представлен в главе 10; известно, что такой подход быстро становится громоздким и сложным. Для решения этой проблемы предлагается использовать новый класс, но здесь используются "статические" методы. В этих методах обычные исключения захватываются и обрабатываются соответственно. Эти методы делают остальной код более чистым и легче воспринимаемым.Первый класс, помогающий решить эту задачу, называется `SourceCodeFile` (файл исходного кода), он представляет собой всю информацию о файле исходного кода данной книги (содержание, имя файла и директория). Он также содержит ряд констант типа `String`, представляющих начало и конец файла; метку для использования внутри упакованного файла; символ новой строки текущей системы; разделитель путей файлов (необходимо использовать `System.getProperty()` для определения локального значения); а также длинное заявление об авторских правах, которое было взято из следующего файла `Copyright.txt`:

```java
///////////////////////////////////////////////////////////////////
// Авторское право © Брюса Экеля, 1998
// Файл исходного кода из книги "Java. Программирование мысли"
// Все права защищены, кроме того, что указано ниже:
// Вы можете свободно использовать этот файл для своей работы (личной или коммерческой),
// включая модификации и распространение в исполняемой форме только.
// Разрешено использование этого файла в учебных целях, включая его использование в материалах презентаций,
// при условии, что книга "Java. Программирование мысли" указана как источник.
// За исключением учебных целей, вы не имеете права копировать и распространять этот код;
// вместо этого единственная точка распространения — это http://www.BruceEckel.com
// (и официальные зеркальные сайты), где он доступен бесплатно.
// Вы не должны удалять это авторское сообщение.
```// Вы не имеете права распространять модифицированные версии исходного кода в этом пакете.
// Вы не имеете права использовать этот файл в печатных изданиях без явного разрешения автора.
// Брюс Экель не делает никаких заявлений относительно подходящести этого программного обеспечения для любой цели.
// Оно предоставляется "как есть", без явных или подразумеваемых гарантий любого типа, включая любую подразумеваемую гарантию
// соответствия требованиям, пригодности для конкретных целей или отсутствия нарушений прав.
// Полный риск относительно качества и производительности программного обеспечения лежит на вас.
// Брюс Экель и издательство не будут нести ответственность за любые убытки, понесенные вами или третьими лицами вследствие использования или распространения программного обеспечения.
// Ни при каких условиях Брюс Экель или издательство не будут нести ответственности за потерю дохода, прибыли или данных, а также за прямые, косвенные, специальные, последственное, случайное или наказательное возмещение убытков,
// независимо от теории ответственности, возникающие из-за использования или невозможности использования программного обеспечения, даже если Брюс Экель и издательство были проинформированы о возможности таких убытков.// Если вам кажется, что вы нашли ошибку, пожалуйста, отправьте все модифицированные файлы с подробно комментируемыми изменениями на адрес:
 // Bruce@EckelObjects.com. (пожалуйста, используйте тот же адрес для ошибок, найденных в книге, но не связанных с кодом).
 //////////////////////////////////////////////////
```При извлечении файлов из архивного файла также указывается разделитель пути файлов, который использовался в исходной системе, чтобы можно было заменить его на подходящий для локальной системы.```Подкаталог текущей главы хранится в поле `chapter`, которое инициализируется значением `c02` (обратите внимание, что список второй главы не содержит пакетного объявления). Поле `chapter` изменяется только тогда, когда в текущем файле встречается ключевое слово `package`.

(1) Создание пакетного файла

Первый конструктор используется для извлечения файла из ASCII-версии этой книги. Вызывающий код (на более глубоком уровне списка) читает каждую строку до тех пор, пока не найдет ту, которая совпадает с началом списка. В этот момент создается объект `SourceCodeFile`, первому содержанию строки передается значение (уже прочтенное вызывающим кодом), а также передается объект `BufferedReader` для извлечения оставшейся части списка из буфера.С этого момента метод `String` будет часто использоваться. Для извлечения имени файла используется перегруженная версия `substring()`, которая начинает чтение с начального смещения и продолжает до конца строки, образуя "подстроку". Чтобы вычислить это начальное смещение, сначала используется `length()` для получения общей длины `startMarker`, затем применяется `trim()` для удаления лишних пробелов в начале и конце строки. Первый ряд символов после имени файла может содержать некоторые символы; они обнаруживаются с помощью `indexOf()`. Если нужный символ не найден, возвращается -1; если он найден, возвращается его первое положение. Обратите внимание, что это также является перегруженной версией `indexOf()`, которая принимает строку как параметр, а не отдельный символ.Извлечённое имя файла сохраняется, а первая строка помещается в строку `contents` (которая используется для хранения всего текста списка исходного кода). Затем остальные строки кода читаются и добавляются в строку `contents`. Конечно, всё не так просто, поскольку специальные случаи требуют особого контроля. Один из таких случаев  проверка ошибок: если сразу следует `startMarker` (маркер начала), это означает, что текущий список исходного кода не имеет завершающего маркера. Это является условием ошибки, при котором программа должна завершиться. Другое особое условие связано с ключевым словом `package`. Несмотря на то что Java  это свободноформатный язык, этот программный код требует, чтобы ключевое слово `package` находилось в начале строки. При обнаружении ключевого слова `package` имя пакета можно извлечь, проверив пробелы в начале строки и наличие точки с запятой в конце (или выполнить эту операцию за один проход, используя перегруженную версию метода `substring()`, которая одновременно проверяет начальные и конечные индексы). Затем все точки в имени пакета заменяются специальным разделителем файлов  при этом предполагается, что разделитель имеет длину всего одного символа. Хотя данное предположение может быть справедливым для всех текущих систем, стоит помнить о необходимости его проверки при возникновении проблем.По умолчанию каждая строка соединяется с `contents`, включая символ новой строки, до тех пор, пока не будет достигнут маркер завершения (`endMarker`). Этот маркер указывает конструктору остановиться. Если конец файла встречен раньше, чем `endMarker`, это считается ошибкой.(2) Извлечение из архива

Второй конструктор используется для восстановления исходных файлов из архивированного файла. Здесь вызывающий метод не беспокоится о пропущенных промежуточных данных. Архивированный файл содержит все исходные файлы, расположенные рядом друг с другом. Конструктору передаётся всего лишь объект `BufferedReader`, представляющий "источник информации". Конструктор извлекает из него необходимую информацию. Однако перед каждой последовательностью кода есть некоторое конфигурационное описание, которое отмечено как `packMarker`. Если `packMarker` отсутствует, это значит, что вызывающий метод пытается использовать этот конструктор неправильно.

Как только встречается `packMarker`, он удаляется, и извлекаются имя директории (завершающееся символом `#`) и имя файла (до конца строки). В любом случае старый разделитель заменяется местным, используя метод `String replace()`. Старый разделитель находится в начале архивированного файла, и его извлечение можно видеть чуть ниже в тексте.

Оставшаяся часть конструктора очень проста. Он читает каждую строку, объединяет её с `contents`, до тех пор, пока не встретит `endMarker`.

(3) Доступ к спискам программСледующие методы являются простыми accessor'ами: `directory()`, `filename()` (обратите внимание, что метод может иметь такое же имя и регистр, как поле) и `contents()`. Метод `hasFile()` используется для указания, содержит ли данный объект файл (вскоре станет понятно, почему это важно).Последние три метода направлены на запись этого списка кода в файл  либо через `writePacked()` в архивированный файл, либо через `writeFile()` в Java-файл исходного кода. Метод `writePacked()` требует единственного параметра  `DataOutputStream`, который открыт где-то ещё и представляет собой файл для записи. Он помещает заголовочные данные в первую строку, затем вызывает `writeBytes()`, чтобы записать `contents` в "упакованном" формате. При подготовке к записи Java-источников необходимо сначала создать файл. Это реализуется с помощью `IO.psOpen()`. Нам нужно передать ему объект типа `File`, который включает как имя файла, так и информацию о пути. Однако вопрос заключается в том, существует ли этот путь фактически? Пользователи могут решить поместить все каталоги исходного кода в совершенно другой подкаталог, который может еще не существовать. Поэтому перед записью каждого файла следует вызвать метод `File.mkdirs()`, чтобы создать необходимую директорию. Этот метод позволяет создать всю цепочку директорий за один раз.```markdown
Перед тем как начать запись Java-источников, необходимо сначала создать файл. Это достигается с помощью `IO.psOpen()`. Вам нужно передать ему объект типа `File`, содержащий имя файла и информацию о пути. Но возникает вопрос  действительно ли этот путь существует? Пользователи могут выбрать помещение всех каталогов исходного кода в совершенно другой подкаталог, который может ещё не существовать. Поэтому перед записью каждого файла следует вызвать метод `File.mkdirs()`, чтобы создать требуемую директорию. Этот метод позволяет создать всю цепочку директорий за один раз.
```
Полное включение списка каталоговОрганизация списков кода с помощью подкаталогов очень удобна, хотя это требует создания полного списка в памяти заранее. Есть ещё одна веская причина для этого  создание более "здорового" системы. То есть, при создании каждого подкаталога списка кода, добавляется дополнительный файл, имя которого указывает на количество файлов, которое должно присутствовать в этом каталоге.

Класс `DirMap` помогает нам реализовать эту идею и эффективно демонстрирует концепцию "множественного отображения". Это достигается с помощью хэш-таблицы (`Hashtable`), где ключами являются подкаталоги, а значениями  объекты типа `Vector`, содержащие объекты `SourceCodeFile`. Таким образом, мы здесь не просто отображаем ключ на значение, но используем `Vector`, чтобы множественно отобразить ключ на серию значений. Хотя это может звучать сложным, его конкретная реализация довольно проста и прямолинейна. Большая часть кода класса `DirMap` связана с записью данных в файл, а не с самой концепцией "множественного отображения".

Связь `DirMap` (отображение директорий) можно установить двумя способами: по умолчанию конструктор предполагает, что мы хотим расширять каталоги от текущего положения вниз, а другой конструктор позволяет нам указать альтернативный абсолютный путь начального каталога.Метод `add()` является местом, где происходит множество действий. Сначала извлекается `directory()` из файла `SourceCodeFile`, который мы хотим добавить, затем проверяется хэш-таблица (`Hashtable`) на наличие данного ключа. Если такого ключа нет, то добавляется новый `Vector` в хэш-таблицу и он связывается с этим ключом. В любом случае, после этих шагов `Vector` уже готов к использованию для добавления `SourceCodeFile`. Благодаря тому, как `Vector` легко объединяется с хэш-таблицей, работа с ними становится удобной.

При записи архива файлов открывается файл для записи (например, `DataOutputStream`, чтобы данные были "универсальными"), и в первой строке записывается заголовочная информация относительно старых разделителей. Затем генерируется перечень ключей хэш-таблицы (`Enumeration`), и каждый каталог перебирается, получая `Vector`, связанную с каждым каталогом, и каждый `SourceCodeFile` внутри этого `Vector` записывается в архивный файл. Используя `write()`, Java-источники записываются в соответствующие директории методами, практически идентичными `writePackedFile()`, поскольку оба метода просто вызывают соответствующие методы внутри объекта `SourceCodeFile`. Однако здесь корневой путь передается методу `SourceCodeFile.writeFile()`. После записи всех файлов, дополнительный файл с указанием количества успешно записанных файлов также будет создан.### Главная программа

Рассмотренные выше классы используются в `CodePackager`. В первую очередь отображается строка использования. Когда конечный пользователь неправильно запускает программу, эта строка используется для вывода правильного способа её использования. Эта строка вызывается методом `usage()`, который также завершает выполнение программы. Единственная задача метода `main()`  определить, хотим ли мы создать архивный файл или извлечь что-то из него. Затем он гарантирует использование правильных параметров и вызывает соответствующий метод.

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

1. Если начало строки представляет собой маркер начала списка источников, создаётся новый объект `SourceCodeFile`. Конструктор читает остальную часть списка источников. Полученный объект добавляется напрямую в `DirMap`.

2. Если начало строки представляет собой маркер окончания списка источников, это указывает на ошибку, так как этот маркер должен быть найден конструктором `SourceCodeFile`.При извлечении/распаковке архивного файла содержимое может быть помещено в текущую директорию или в другую заранее подготовленную директорию. Поэтому создаётся соответствующий объект `DirMap`. Открыв файл, первая строка считывается. Информация о разделителях путей файлов извлекается из этой строки. Затем создаётся первый объект `SourceCodeFile` согласно входным данным и добавляется в `DirMap`. Для каждого последующего файла создаётся новый объект `SourceCodeFile`, который также добавляется (последний созданный объект после полного использования входных данных просто завершает работу, и `hasFile()` возвращает ошибку).## 17.1.2 Проверка стиля написания

Хотя пример выше кажется удобным для некоторых проектов, связанных с текстовым процессингом, следующий пример сразу же выполняет свою роль, выполняя проверку стиля, чтобы гарантировать, что наши формы написания соответствуют "стандарту стиля Java". Он открывает каждый `.java` файл в текущей директории и извлекает все имена классов и идентификаторы. Если обнаруживаются случаи, нарушающие стандарт Java, они сообщаются нам. Чтобы этот программный модуль работал правильно, сначала следует создать имя класса, которое будет использоваться как «склад», отвечающий за хранение всех имён классов в стандартной библиотеке Java. Для достижения этой цели требуется пройтись по всем подкаталогам, используемым для стандартной библиотеки Java, и запустить `ClassScanner` в каждом из них. В качестве аргументов предоставляются имя файла склада (которое всегда остаётся тем же самым) и ключ командной строки `-a`, указывающий на то, что имена классов должны быть добавлены в этот файл склада.

Для проверки своего кода с помощью программы её необходимо запустить и передать путь и имя файла склада. Программа затем проверяет все классы и идентификаторы в текущей директории и сообщает, какие из них не следуют типичному правилу записи Java.Необходимо отметить, что эта программа не идеальна. Иногда она может сообщить о найденной проблеме, но при более внимательном рассмотрении кода оказывается, что ничего менять не нужно. Хотя это может быть немного раздражающим, всё равно лучше, чем самостоятельная проверка ошибок во всём коде.

Ниже представлены исходные коды, за которыми следует подробное объяснение:

```java
//: ClassScanner.java
// Проходит по всем файлам в каталоге для поиска классов
// и идентификаторов, чтобы проверить правила записи.
// Предполагает корректно компилируемые примеры кода.
// Не делает всё правильно, но является очень полезным помощником.
import java.io.*;
import java.util.*;

class MultiStringMap extends Hashtable {
  public void add(String key, String value) {
    if (!containsKey(key))
      put(key, new Vector());
    ((Vector) get(key)).addElement(value);
  }

  public Vector getVector(String key) {
    if (!containsKey(key)) {
      System.err.println("Ошибка: ключ не найден: " + key);
      System.exit(1);
    }
    return (Vector) get(key);
  }
}
``````java
public class ClassScanner {
    private File path;
    private String[] fileList;
    private Properties classes = new Properties();
    private MultiStringMap
        classMap = new MultiStringMap(),
        idMap = new MultiStringMap();
    private StreamTokenizer input;

    public ClassScanner() {
        path = new File(".");
        fileList = path.list(new JavaFilter());
        for (int i = 0; i < fileList.length; i++) {
            System.out.println(fileList[i]);
            scanFile(fileList[i]);
        }
    }

    void scanFile(String fileName) {
        try {
            input = new StreamTokenizer(
                new BufferedReader(
                    new FileReader(fileName)));
            // Doesn't work:
            // input.slashStarComments(true);
            // input.slashSlashComments(true);
            input.whitespaceChars('/', '/');
            input.wordChars('.', '.');
            input.eolIsSignificant(true);

            while (input.nextToken() != 
                   StreamTokenizer.TT_EOF) {
                if (input.ttype == '/') 
                    eatComments();
                else_if(input.ttype == 
                        StreamTokenizer.TT_WORD) {
                    if (input.sval.equals("class") ||
                        input.sval.equals("interface")) {
```
Обратите внимание, что `ещё_если` должно быть заменено на `else if`.```markdown
                        // Получаем имя класса:
                             а пока (вход.следующийТокен() != 
                                    StreamTokenizer.TT_EOF && 
                                    вход.типТокена() != 
                                    StreamTokenizer.TT_WORD)
                                 ;
                             классы.положить(вход.значение(), вход.значение());
                             классМап.добавить(имяФайла, вход.значение());
                     }
                     если (вход.значение().equals("импорт") || 
                         вход.значение().equals("пакет"))
                         игнорироватьСтроку();
                     ещё // Это идентификатор или ключевое слово
                         идентМап.добавить(имяФайла, вход.значение());
                 }
             }
         } захват (IOException e) {
             e.printStackTrace();
         }
     }
     void игнорироватьСтроку () {
         попробуй {
             а пока (вход.следующийТокен() != 
                    StreamTokenizer.TT_EOF && 
                    вход.типТокена() != 
                    StreamTokenizer.TT_EOL)
                 ; // Отбрасываем токены до конца строки
         } захват (IOException e) {
             e.printStackTrace();
         }
     }
     // Удаление комментариев StreamTokenizer'ом кажется
     // поломанным. Это извлекает их:
     void съестьКомментарии () {
         попробуй {
             если (вход.следующийТокен() != 
                  StreamTokenizer.TT_EOF) {
                 если (вход.типТокена() == '/') 
                     игнорироватьСтроку ();
                 ещё_если (вход.типТокена() != '*') 
                     вход.вернутьНазад ();
                 ещё
                     а_пока (истинно) {
```
```markdown
                        если (вход.следующийТокен() == 
                             StreamTokenizer.TT_EOF) {
                            выход;
                        }
                    }
            }
        } захват (IOException e) {
            e.printStackTrace();
        }
    }
}
```

```markdown
              StreamTokenizer.TT_EOF)
               break;
             if (in.ttype == '*') {
               if (in.nextToken() != 
                   StreamTokenizer.TT_EOF
                   && in.ttype == '/') {
                 break;
               }
             }
           }
       }
     } catch (IOException e) {
       e.printStackTrace();
     }
   }

   public String[]classNames() {
     String[]result = new String[classes.size()];
     Enumeration e = classes.keys();
     int i = 0;
     while (e.hasMoreElements()) {
       result[i++] = (String)e.nextElement();
     }
     return result;
   }

   public void checkClassNames() {
     Enumeration files = classMap.keys();
     while (files.hasMoreElements()) {
       String file = (String)files.nextElement();
       Vector cls = classMap.getVector(file);
       for (int i = 0; i < cls.size(); i++) {
         String className = 
           (String)cls.elementAt(i);
         if (Character.isLowerCase(className.charAt(0))) {
           System.out.println(
             "Ошибка в написании имени класса, файл: "
             + file + ", класс: "
             + className);
         }
       }
     }
   }
```   ```java
   public void checkIdentNames() {
     Enumeration files = identMap.keys();
     Vector reportSet = new Vector();
     while (files.hasMoreElements()) {
       String file = (String) files.nextElement();
       Vector ids = identMap.getVector(file);
       for (int i = 0; i < ids.size(); i++) {
         String id = (String) ids.elementAt(i);
         if (!classes.contains(id)) {
           // Skip identifiers that are three or more characters long and consist entirely of uppercase letters (likely constants):
           if (id.length() >= 3 && id.equalsIgnoreCase(id.toUpperCase())) {
             continue;
           }
           // Check the first character to see if it's uppercase:
           if (Character.isUpperCase(id.charAt(0))) {
             if (reportSet.indexOf(file + id) == -1) { // Not reported yet
               reportSet.addElement(file + id);
               System.out.println("Error in identifier name writing in file:" 
                                 + file + ", identifier: " + id);
             }
           }
         }
       }
     }
   }
   ```
   
Файл был переведён согласно правилам:

- Тексты внутри комментариев были переведены.
- Сообщения об ошибках были переведены.
- Все остальные части кода остались без изменений.```markdown
   private static final String usage =
     "Использование:\n"
     + "ClassScanner classnames -a\n"
     + "\tДобавляет все имена классов в этой директории в файл репозитория с названием 'classnames'\n"
     + "ClassScanner classnames\n";
```

```markdown
Проверяет все файлы Java в этой
директории на наличие ошибок с заглавными буквами,
используя файл репозитория 'classnames'.
private static void usage() {
  System.err.println(usage);
  System.exit(1);
}
public static void main(String[] args) {
  if(args.length < 1 || args.length > 2)
    usage();
  ClassScanner c = new ClassScanner();
  File old = new File(args[0]);
  if(old.exists()) {
    try {
      // Попытка открыть существующий
      // файл свойств:
      InputStream oldlist =
        new BufferedInputStream(new FileInputStream(old));
      c.classes.load(oldlist);
      oldlist.close();
    } catch(IOException e) {
      System.err.println("Не удалось открыть " + old + " для чтения");
      System.exit(1);
    }
  }
  if(args.length == 1) {
    c.checkClassNames();
    c.checkIdentNames();
  }
  // Записывает названия классов в репозиторий:
  if(args.length == 2) {
    if(!args[1].equals("-a"))
      usage();
    try {
      BufferedOutputStream out =
        new BufferedOutputStream(new FileOutputStream(args[0]));
      c.classes.save(out, "Classes found by ClassScanner.java");
      out.close();
    } catch(IOException e) {
      System.err.println("Не удалось записать " + args[0]);
      System.exit(1);
    }
  }
}
```
```markdown
Класс `JavaFilter` реализует интерфейс `FilenameFilter`.
```java
class JavaFilter implements FilenameFilter {
  public boolean accept(File dir, String name) {
    // Удаление информации о пути:
    String f = new File(name).getName();
    return f.trim().endsWith(".java");
  }
}
```
///~
```---

Класс `MultiStringMap` является специальным инструментом, который позволяет нам связывать набор строк с каждым ключом (мапой). Как и в предыдущем примере, здесь используется хэш-таблица (`Hashtable`), но теперь она наследуется. Эта хэш-таблица рассматривает ключ как единственный строковый объект, который отображается в значение типа `Vector`. Метод `add()` выполняет простую проверку наличия ключа в хэш-таблице. Если ключ отсутствует, он добавляется. Метод `getVector()` создаёт `Vector` для конкретного ключа; метод `printValues()` выводит все значения последовательно для каждого `Vector`, что полезно для отладки программы.

Для упрощения программы все имена классов из стандартной библиотеки Java помещаются в объект `Properties` (свойства) (из стандартной библиотеки Java). Вспомните, что объект `Properties` фактически представляет собой хэш-таблицу, содержащую только строки для ключей и значений. Однако всего одним вызовом метода мы можем сохранить его на диск или восстановить с диска. На самом деле нам требуется только список имен, поэтому для ключей и значений используются одинаковые объекты.Для определённого каталога файлов мы используем два объекта `MultiStringMap`: `classMap` и `identMap`. При запуске программы стандартная библиотека классов загружается в объект `Properties` с именем `classes`. Когда новый класс обнаруживается в локальном каталоге, он также добавляется в `classes` и `classMap`. Таким образом, `classMap` может использоваться для прохождения через все классы в локальном каталоге, а `classes` можно использовать для проверки текущего имени класса (оно указывает начало определения объекта или метода, так что следующие токены собираются до встречи точки с запятой и помещаются в `identMap`).По умолчанию конструктор класса `ClassScanner` создаёт список файлов (в форме реализации интерфейса `FilenameFilter` классом `JavaFilter`, см. главу 10). Затем для каждого имени файла вызывается метод `scanListing()`.

```Внутри `scanListing()`, открывается исходный файл, который затем преобразуется в объект `StreamTokenizer`. По информации Java API, передача `true` методам `slashStartComments()` и `slashSlashComments()` должна была означать удаление комментариев, но это работает недостаточно эффективно (почти бесполезно в Java 1.0). Вместо этого строки помечаются как комментарии, а выделение комментариев выполняется другим способом. Для достижения этой цели символ `'/'` должен быть зафиксирован как обычный символ с помощью метода `ordinaryChar()`, а не рассматриваться `StreamTokenizer` как часть комментария. То же самое относится к символу точки (`'.'`), так как нам требуется разделение вызова метода на отдельные идентификаторы. Однако для символа подчеркивания он первоначально воспринимается `StreamTokenizer` как отдельный символ, но здесь его следует оставить как часть идентификатора, поскольку он используется в значениях типа `static final`, таких как `TT_EOF`. Конечно, это справедливо только для данного специфического приложения. Метод `wordChars()` требует указания последовательности символов, которые должны быть сохранены как часть одного слова.```

```Внутри `scanListing()`, открывается исходный файл, который затем преобразуется в объект `StreamTokenizer`. По информации Java API, передача `true` методам `slashStartComments()` и `slashSlashComments()` должна была означать удаление комментариев, но это работает недостаточно эффективно (почти бесполезно в Java 1.0). Вместо этого строки помечаются как комментарии, а выделение комментариев выполняется другим способом. Для достижения этой цели символ `/` должен быть зафиксирован как обычный символ с помощью метода `ordinaryChar()`, а не рассматриваться `StreamTokenizer` как часть комментария. То же самое относится к символу точки (`.`), так как нам требуется разделение вызова метода на отдельные идентификаторы. Однако для символа подчеркивания он первоначально воспринимается `StreamTokenizer` как отдельный символ, но здесь его следует оставить как часть идентификатора, поскольку он используется в значениях типа `static final`, таких как `TT_EOF`. Конечно, это справедливо только для данного специфического приложения. Метод `wordChars()` требует указания последовательности символов, которые должны быть сохранены как часть одного слова.```Наконец, чтобы знать, когда происходит переход на новую строку при анализе однострочных комментариев или пропуске строки, необходимо использовать метод `eollsSignificant(true)`, чтобы символ конца строки (`EOL`) был представлен явно, а не поглощён `StreamTokenizer`. Оставшаяся часть `scanListing()` будет читать и проверять токены до конца файла. Как только `nextToken()` вернёт `final static` значение  `StreamTokenizer.TT_EOF`, это будет указывать на то, что конец файла достигнут.Если токен представляет собой `'/'`, это может означать начало комментария, поэтому вызывается метод `eatComments()`. В других случаях нас интересует только то, является ли токен словом. Конечно, могут существовать и другие специальные случаи.

Если слово равно `class` (класс) или `interface` (интерфейс), следующий токен должен представлять имя класса или интерфейса соответственно, которое затем помещается в `classes` и `classMap`. Если слово равно `import` или `package`, нам больше ничего не интересно в этой строке. Все остальное должно быть идентификатором (что нас интересует) или ключевым словом (на которое мы не обращаем внимания, так как они всегда пишутся строчными буквами, поэтому нет необходимости выполнять дополнительную проверку). Они добавляются в `identMap`.

Метод `discardLine()` служит простым инструментом для поиска конца строки. Обратите внимание, что каждый раз при получении нового токена следует проверять конец строки.Каждый раз, когда в основной цикл парсинга попадает слэш '/', вызывается метод `eatComments()`. Однако это не обязательно означает, что встречен комментарий, поэтому следует извлечь следующий токен и проверить, является ли он слэшем (в этом случае строка будет отброшена) или звездой. Но если ни одно из этих условий не выполняется, значит, необходимо вернуть обратно в основной цикл парсинга тот токен, который было извлечено! К счастью, метод `pushBack()` позволяет "вернуть" текущий токен обратно в поток входных данных. Таким образом, при вызове `nextToken()` в основном цикле парсинга, он правильно получит тот токен, который был отправлен назад.Для удобства метод `classNames()` создает массив, содержащий все имена из коллекции `classes`. Этот метод не используется в программе, но очень полезен для отладки кода.

Следующие два метода являются местом, где происходит фактическая проверка. В методе `checkClassNames()` имена классов извлекаются из `classMap` (помните, что `classMap` содержит только имена внутри данного каталога, организованные по имени файла, поэтому имя файла может содержать ошибочные имена классов). Для этого требуется извлечение каждого связанного `Vector` и последующее его прохождение, чтобы проверить, начинается ли первый символ со строчной буквы. Если это действительно так, выводится сообщение об ошибке. В методе `checkIdentNames()`, мы используем аналогичный подход: каждое имя идентификатора извлекается из `identMap`. Если имя отсутствует в списке `classes`, то считается, что это либо идентификатор, либо ключевое слово. В этом случае проверяется специальное условие: если длина имени идентификатора равна 3 или более, а все его символы являются заглавными буквами, то такой идентификатор игнорируется, так как он может представлять собой константу типа `static final`, например `TT_EOF`. Конечно, это не идеальный алгоритм, но он предполагает, что любое имя идентификатора, состоящее полностью из заглавных букв, является некорректным.Этот метод не предназначен для отчета обо всех идентификаторах, начинающихся с заглавной буквы, а просто отслеживает те, которые уже были отмечены в `Vector` с помощью метода `reportSet()`. Этот `Vector` рассматривается как "множество", которое указывает, был ли данный элемент уже добавлен в него. Элемент создается путем объединения имени файла и имени идентификатора. Если элемент еще не присутствует в множестве, он добавляется, после чего генерируется отчет.

Оставшаяся часть программы состоит из метода `main()`, который управляет командными строковыми параметрами и решает, будем ли мы строить "кладбище" классов на основе стандартной библиотеки Java, или же хотим проверить корректность уже написанного кода. В любом случае, будет создан объект `ClassScanner`.Независимо от того, собираемся ли мы строить "кладбище" или использовать существующее, нам придётся попытаться открыть существующее "кладбище". Это достигается путём создания объекта `File` и тестирования его наличия; затем файл открывается, и список `classes` типа `Properties` загружается в `ClassScanner` (используется метод `load()`). Классы из "кладбища" будут добавлены к списку классов, найденному конструктором `ClassScanner`, вместо того чтобы заменить его. Если предоставлен только один командный параметр, значит, требуется проверка имен классов и идентификаторов. Однако при предоставлении двух параметров (второй  `-a`) требуется создание "кладбища" имен классов. В этом случае потребуется открытие выходного файла и запись списка с использованием метода `Properties.save()`, а также передача строки с информацией о заголовке файла.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-thinking-in-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-thinking-in-java-zh.git
oschina-mirror
wizardforcel-thinking-in-java-zh
wizardforcel-thinking-in-java-zh
master