Философия Java

         

Проверка стиля капитализации


В этом разделе мы взглянем на более сложный пример использования ввода/вывода в Java, который также использует токенизацию. Этот проект весьма полезен, потому что он выполняет проверку стиля, чтобы убедится, что ваша капитализация соответствует стилю Java, который можно найти на java.sun.com/docs/codeconv/index.html. Он открывает .java файл в текущем директории и извлекает все имена классов и идентификаторов, затем показывает, если какой-то из них не соответствует стилю Java.

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

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

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

//: c11:ClassScanner.java

// Сканирует все файлы в директории в поисках

// классов и идентификаторов для проверки капитализации.

// Принимает правильно составленные списки кода.

// Не все делает правильно, но достаточно хороший помощник.

import java.io.*; import java.util.*;

class MultiStringMap extends HashMap { public void add(String key, String value) { if(!containsKey(key)) put(key, new ArrayList()); ((ArrayList)get(key)).add(value); } public ArrayList getArrayList(String key) { if(!containsKey(key)) { System.err.println( "ERROR: can't find key: " + key); System.exit(1); } return (ArrayList)get(key); } public void printValues(PrintStream p) { Iterator k = keySet().iterator(); while(k.hasNext()) { String oneKey = (String)k.next(); ArrayList val = getArrayList(oneKey); for(int i = 0; i < val.size(); i++) p.println((String)val.get(i)); } } }


public class ClassScanner { private File path; private String[] fileList; private Properties classes = new Properties(); private MultiStringMap classMap = new MultiStringMap(), identMap = new MultiStringMap(); private StreamTokenizer in; public ClassScanner() throws IOException { path = new File("."); fileList = path.list(new JavaFilter()); for(int i = 0; i < fileList.length; i++) { System.out.println(fileList[i]); try { scanListing(fileList[i]); } catch(FileNotFoundException e) { System.err.println("Could not open " + fileList[i]); } } } void scanListing(String fname) throws IOException { in = new StreamTokenizer( new BufferedReader( new FileReader(fname))); // Кажется, не работает:

// in.slashStarComments(true);

// in.slashSlashComments(true);

in.ordinaryChar('/'); in.ordinaryChar('.'); in.wordChars('_', '_'); in.eolIsSignificant(true); while(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') eatComments(); else if(in.ttype == StreamTokenizer.TT_WORD) { if(in.sval.equals("class") || in.sval.equals("interface")) { // Получаем имя класса:

while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_WORD) ; classes.put(in.sval, in.sval); classMap.add(fname, in.sval); } if(in.sval.equals("import") || in.sval.equals("package")) discardLine(); else // Это идентификатор или ключевое слово



identMap.add(fname, in.sval); } } } void discardLine() throws IOException { while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_EOL) ; // Выбрасываем элемент в конец строки

} // Кажется, что метод удаления комментариев StreamTokenizer

// сломан. Это извлекает комментарии:

void eatComments() throws IOException { if(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') discardLine(); else if(in.ttype != '*') in.pushBack(); else while(true) { if(in.nextToken() == StreamTokenizer.TT_EOF) break; if(in.ttype == '*') if(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype == '/') break; } } } public String[] classNames() { String[] result = new String[classes.size()]; Iterator e = classes.keySet().iterator(); int i = 0; while(e.hasNext()) result[i++] = (String)e.next(); return result; } public void checkClassNames() { Iterator files = classMap.keySet().iterator(); while(files.hasNext()) { String file = (String)files.next(); ArrayList cls = classMap.getArrayList(file); for(int i = 0; i < cls.size(); i++) { String className = (String)cls.get(i); if(Character.isLowerCase( className.charAt(0))) System.out.println( "class capitalization error, file: "



+ file + ", class: " + className); } } } public void checkIdentNames() { Iterator files = identMap.keySet().iterator(); ArrayList reportSet = new ArrayList(); while(files.hasNext()) { String file = (String)files.next(); ArrayList ids = identMap.getArrayList(file); for(int i = 0; i < ids.size(); i++) { String id = (String)ids.get(i); if(!classes.contains(id)) { // Игнорирует идентификаторы длиной 3 или

// более символов, если они все в верхнем регистре

// (эероятно это значения static final):

if(id.length() >= 3 && id.equals( id.toUpperCase())) continue; // Проверяется, записан ли первый символ в верхнем регистре:

if(Character.isUpperCase(id.charAt(0))){ if(reportSet.indexOf(file + id) == -1){ // Еще не включено в отчет

reportSet.add(file + id); System.out.println( "Ident capitalization error in:"

+ file + ", ident: " + id); } } } } } } static final String usage = "Usage: \n" + "ClassScanner classnames -a\n" + "\tAdds all the class names in this \n" + "\tdirectory to the repository file \n" + "\tcalled 'classnames'\n" + "ClassScanner classnames\n" + "\tChecks all the java files in this \n" + "\tdirectory for capitalization errors, \n" + "\tusing the repository file 'classnames'"; private static void usage() { System.err.println(usage); System.exit(1); } public static void main(String[] args) throws IOException { 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("Could not open "

+ old + " for reading"); 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.store(out, "Classes found by ClassScanner.java"); out.close(); } catch(IOException e) { System.err.println( "Could not write " + args[0]); System.exit(1); } } } }

class JavaFilter implements FilenameFilter { public boolean accept(File dir, String name) { // Strip path information:

String f = new File(name).getName(); return f.trim().endsWith(".java"); } } ///:~

Класс MultiStringMap является инструментом, позволяющим вам ставить в соответствие группу строк и каждое ключевое включение. Он использует HashMap (в этот раз через наследование). В качестве ключевых значений используются единичные строки, которые ставятся в соответствие значению ArrayList. Метод add( ) просто проверяет, есть ли уже такое ключевое значение в HashMap, а если его нет, помещает его туда. Метод getArrayList( ) производит ArrayList определенных ключей, а printValues( ), который особенно полезен для отладки, печатает все значения ArrayList, получая ArrayList.

Для облегчения жизни все имена классов стандартной библиотеки Java помещаются в объект Properties (из стандартной библиотеки Java). Помните, что объект Properties является типом HashMap, который хранит только объекты String и для ключевого значения, и для хранимого элемента. Однако он может быть сохранен на диске и восстановлен с диска в одном вызове метода, так что он идеален в качестве хранилища имен. На самом деле нам нужен только список имен, но HashMap не может принимать null ни для ключевых значений, ни для хранящихся значений. Так что один и тот же объект будет использоваться и для ключа, и для значения.

Для классов и идентификаторов, которые будут обнаружены в определенном директории, используются две MultiStringMap: classMap и identMap. Также, когда запускается программа, она загружает хранилище стандартных имен классов в объект Properties, называемый classes, а когда обнаруживается новое имя класса в локальном директории, то оно добавляется и в classes, и в classMap. Таким образом, classMap может использоваться для обхода всех классов в локальном директории, а classes может использоваться для проверки, является ли текущий значащий элемент именем класса (что указывается определением объекта или началом метода, так как захватывается следующий значащий элемент — до точки с запятой — и помещается в identMap).



Конструктор по умолчанию для ClassScanner создает список имен, используя JavaFilter, показанный в конце файла, который реализует интерфейс FilenameFilter. Затем вызывается scanListing( ) для каждого имени файла.

Внутри scanListing( ) открывается файл исходного кода и передается в StreamTokenizer. В документации есть функции slashStarComments( ) и slashSlashComments( ), предназначенные для отсеивания коментариев, которым передается true, но это выглядит некорректно, так как это плохо работает. Поэтому эти строки закомментированы, а комментарии извлекаются другим методом. Чтобы извлечь комментарий, “/” должен трактоваться как обычный символ, и нужно не позволять StreamTokenizer собирать его как часть комментария, поэтому метод ordinaryChar( ) говорит StreamTokenizer, чтобы он не делал это. Это также верно в отношении точки (“.”), так как мы хотим иметь метод, который бы извлекал индивидуальные идентификаторы. Однако символ подчеркивания, который трактуется StreamTokenizer как индивидуальный символ, должен оставляться как часть идентификатора, так как он появляется в таких значениях типа static final, как TT_EOF, и т. д., очень популярных в этой программе. Метод wordChars( ) принимает диапазон символов, которые вы хотите добавить к остающимся внутри значащего элемента, анализирующегося одним словом. Наконец, когда анализируете однострочный комментарий или обнаруживаете строку, для которой необходимо определить конец строки, то при вызове eolIsSignificant(true) конец строки будет обнаружен раньше, чем он будет получен StreamTokenizer.

Оставшаяся часть scanListing( ) читает и реагирует на значащие элементы, пока не встретится конец файла, которых будет обнаружен, когда nextToken( ) вернет значение final static StreamTokenizer.TT_EOF.

Если значащим элементом является “/”, он потенциально может быть комментарием, так что вызывается eatComments( ), чтобы разобраться с этим. Но нас будут интересовать другие ситуации, когда мы имеем дело со словом, для которого есть несколько специальных случаев.



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

Метод discardLine( ) является простым инструментом, ищущим конец строки. Обратите внимание, что при каждом получении значащего элемента вы должны проверять конец строки.

Метод eatComments( ) вызывается всякий раз, когда обнаружен слеш в главном цикле анализа. Однако это не обязательно означает, что обнаружен комментарий, так что должен быть извлечен следующий значащий элемент, чтобы проверить, не является ли он слешем (в этом случае строка пропускается) или звездочкой. Но если это ни то, ни другое, это означает, что тот значащий элемент, который вы только что извлекли, необходимо вернуть в главный цикл анализа! К счастью, метод pushBack( ) позволяет вам “втолкнуть назад” текущий элемент во входной поток, поэтому, когда главный цикл анализа вызовет nextToken( ), то он получит то, что вы только что втолкнули обратно.

По соглашению, метод classNames( ) производит массив из всех имен, содержащихся в classes. Этот метод не используется в программе, но он очень полезен для отладки.

Следующие два метода относятся к тем, в которых действительно идет проверка. В checkClassNames( ), имя класса извлекается из classMap (который, запомните, содержит только имена их этой директории, организованные по именам файлов, так что имя файла может быть напечатано наряду с беспорядочными именами классов). Это выполняется путем получения каждого ассоциированного ArrayList, и прохода по нему в поисках элементов с меленькой первой буквой. Если такой элемент найден, то печатается соответствующее сообщение об ошибке.

В checkIdentNames( ), используется аналогичный подход; каждое имя идентификатора извлекается из identMap. Если имени нет в списке classes, оно трактуется как идентификатор или ключевое слово. Проверяется особый случай: если длина имени идентификатора больше или равна трем, и все символы являются символами верхнего регистра, этот идентификатор игнорируется, потому что, вероятно, это значение static final, такое как TT_EOF. Конечно, это не идеальный алгоритм, но он означает, что вы будете предупреждены обо всех идентификаторах, записанных в верхнем регистре, и находящихся не на месте.



Вместо сообщения о каждом идентификаторе, который начинается с большой буквы, этот метод хранит историю всего, о чем уже сообщил в ArrayList вызов reportSet( ). Это трактует ArrayList , как “набор”, который говорит вам, встречались ли эти экземпляры в наборе. Экземпляры производятся соединением имени файла и идентификатора. Если элемента нет в наборе, он добавляется, после чего делается сообщение.

Оставшаяся часть текста программы занимается методом main( ), занимается обработкой аргументов командной строки и определяет, хотите ли вы создать хранилище имен из стандартной библиотеки Java, или хотите проверить написанный вами код. В обоих случаях он создает объект ClassScanner.

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


Содержание раздела