проект: "java-учебный лагерь, задание 02.00". структура кода, обработка исключений, JavaDoc
Код рабочий, это мой первый запрос на code-review.
буду сильно признателен любым рекомендациям!
scope:
- по правильной обработке исключений, правильно ли их собрать в класс родитель и создать класс handler, чтобы там уже с ними разобраться?
ExceptionHandler.java
package ex00;
import java.io.FileNotFoundException;
/**
* Отлавливает и определяет тип ошибки, выводит подробное сообщение и
* StackTrace. Утилитный класс.
*/
public final class ExceptionHandler {
private ExceptionHandler() {
throw new UnsupportedOperationException("Utility class");
}
/**
* Принимает выброшенное исключение, определяет его тип и корректно
* обрабатывает его.
*
* @param e
*/
public static void detectException(final Exception e) {
if (e instanceof FileNotFoundException) {
System.out.println(
"There is no file in this path. Details: " + e.getMessage());
} else if (e instanceof SecurityException) {
System.out.println(
"Acces to this file dinied. Details: " + e.getMessage());
} else {
System.out.println(e.getMessage());
}
e.printStackTrace();
}
}
правильно ли я декомпозирую программу на классы и метода внутри них? Не перебарщиваю? Вот структура проекта:

правильно ли я пользуюсь утилитными классами? код таких классов:
выше код ExceptionHandler.java
FileTypeRecorder.java
package ex00; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.List; /** * Записывает тип файла в выходной файл result.txt. Утилитный класс, создание * экземпляров запрщено. */ public class FileTypeRecorder { private FileTypeRecorder() { throw new UnsupportedOperationException("Utility class"); } /** * Побайтово записывает определенный ранее тип заданного файла. В конце * записи добавляет перенос строки. * * @param fileType имя типа файла * @param path путь до файла result.txt * @throws FileNotFoundException если по какой-то причине не смог создать * файл * @throws SecurityException если доступ к файлу ограничен */ public static void recordType(final List<Integer> fileType, final String path) throws IOException { try (FileOutputStream fos = new FileOutputStream(path, true)) { for (int i = 0; i < fileType.size(); i++) { fos.write(fileType.get(i)); } fos.write('\n'); } } }HexStringToByteArrayAdapter.java
package ex00; import java.util.ArrayList; import java.util.List; /** * Утилитный класс. Адаптирует hex-строку с ArrayList<Integer>. */ public final class HexStringToByteArrayAdapter { private HexStringToByteArrayAdapter() { throw new UnsupportedOperationException("Utility lass"); } /** * Конвертирует строку в List<Integer>. * * @param hexString * @return возвращает преобразованный в ints список chars hex-строки */ public static List<Integer> convert(final String hexString) { ArrayList<Integer> adaptedString = new ArrayList<>(); for (int i = 0; i < hexString.length(); i++) { adaptedString.add((int) hexString.charAt(i)); } return adaptedString; } }MagicNumberComparator.java почему-то не могу добавить нормально код, прошу посмотреть по ссылке
- достаточно подробны Javadoc комментарии? (то, что их нужно написать на английском, я уже понял, можно и на английской версии сайта как минимум такой запрос отсавить)
я понял, что код можно не выкладывать тут, а дать ссылку на Гитхаб. Если это не так, не минусуйте пожалуйста (или какая тут есть кара за такое неправильное действие?), я переделаю.
Коротко о том, что нужно было сделать:
Рекомендуемые типы Java Collections API (List, Map<K, V>, и т.д.), InputStream, OutputStream, FileInputStream, FileOutputStream
нужно реализовать приложение для анализа сигнатур произвольных файлов. Эта сигнатура позволяет определить тип содержимого файла и состоит из набора "магических чисел". Эти числа обычно располагаются в начале файла. Например, сигнатура для файла PNG представлена первыми восемью байтами файла, которые одинаковы для всех PNG-изображений:
89 50 4E 47 0D 0A 1A 0A
нужно реализовать приложение, которое принимает signatures.txt в качестве входных данных. В нём содержится список типов файлов и их сигнатур в HEX-формате. Пример (необходимо строго соблюдать указанный формат файла):
PNG, 89 50 4E 47 0D 0A 1A 0A
GIF, 47 49 46 38 37 61
программа должна принимать полные пути к файлам на жёстком диске и определять тип, которому соответствует сигнатура файла. Результат выполнения программы должен быть записан в файл result.txt. Если сигнатура не может быть определена, результат выполнения — UNDEFINED (никакая информация не должна записываться в файл).
Пример работы программы:
$java Program
-> C:/Users/Admin/images.png
PROCESSED
-> C:/Users/Admin/Games/WoW.iso
PROCESSED
-> 42
Содержимое файла result.txt (нет необходимости загружать этот файл как результат):
PNG
GIF
Тут в README подробные условия
Вот код, который прошу посмотреть
ДОПОЛНИТЕЛЬНЫЕ УТВЕРЖДЕНИЯ И ВОПРОСЫ ПОСЛЕ ОБРАТНОЙ СВЯЗИ:
что удалось понять самому:
т.к. требование испоьлзования Java 8 для меня не критически важное, лучше использовать Java 17, он более современный и обладает более широким функционалом.
необходимо наработать навык использования функциональных интерфейсов, такими ка Supplier, Function, Predicate и т.д. Это важно для написания более короткого и понятного кода, для использования в коде лямбда-выражений и Stream API
необходимо наработать навык использования лямбда-выражений и больше их использовать в коде.
необходимо наработать навык использования STREAM API вместо циклов (если я правильно все понял), по крайней мере они делают код более читаемым, в представленной задаче по скорости они не быстрее, но в некоторых случаях могут быть (например в Multithreading)
когда лучше использовать все-таки циклы? про ранний выход понимаю (return, break), может еще какие-то ситуации есть в коммерческой практике?
зачем используется Supplier в аргументах runLoop? моё предположение - для сокращения кода, чтобы в Main'е не создавать лишний объект String.
у меня всегда был вопрос, стоит ли использовать Scanner в try с ресурсами, вы как раз это используете, вопросы: 7.1.почему так нужно делать? ведь после закрытия System.in, к нему уже будет не обратиться, ДАЖЕ если заново попытаться его открыть.
7.2. Понимаю, что он больше будет и не нужен, но почему тогда просто не оставить его открытым? ведь он самостоятельно закроется при выходе из программы.
7.2.1. моё (глупое) предположение - чтобы не ругалась идэешка)
7.2.2. моё предположение - для сокращения и повышения читаемости кода
человек любит пытаться упростить, я человек, пытаюсь: правильно ли я понимаю, что когда происходит работа с вводом, не важно откуда и с помощью какого инструмента, лучше использовать Supplier , чтобы не городить огороды из временных переменных, в которые приходится сохранять считанное?
правильно ли я понимаю, что это - import java.io.UncheckedIOException вы используете как "затычку" просто? и в не учебных проектах лучше обрабатывать все-таки ошибки? но тогда вопрос, но в таком случае почему просто не пробросить исключение наверх?
и финальное, у меня есть проблема с пониманием того, как писать более эффективный код, какие структуры или методы использовать для улучшения эффективности, можете что-то посоветовать, чтобы подтянуть этот вопрос?
Ответы (1 шт):
Если проводить код ревью всего кода, то в нем слишком много проблем, чтобы написать комментарии. Данная реализация должна была выглядеть хотя бы так (хотя доводить ее до идеала просто не хватило времени)
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
import java.util.function.Supplier;
public final class Program {
private static final String STOP_WORD = "42";
private static final Path RESULT_FILE = Paths
.get("result.txt")
.toAbsolutePath();
private static final Path SIGNATURES_FILE = Paths
.get("signatures.txt")
.toAbsolutePath();
public static void main(String[] args) {
SignatureDictionary dictionary = new SignatureDictionary(
SIGNATURES_FILE);
FileSignatureReader reader = new FileSignatureReader(
dictionary.getMaxSize());
try (Scanner scanner = new Scanner(System.in)) {
runLoop(scanner::nextLine, dictionary, reader);
}
}
private static void runLoop(Supplier<String> supplier,
SignatureDictionary dictionary, FileSignatureReader reader) {
String input = "";
while (!input.equals(STOP_WORD)) {
try {
input = supplier.get();
if (input.isBlank() || input.equals(STOP_WORD)) {
break;
}
String signature = reader.readFileSignature(new File(input));
dictionary.getFiletype(signature).ifPresentOrElse(
fileType -> {
FileUtil.writeString(RESULT_FILE,
fileType + System.lineSeparator());
System.out.println("PROCESSED");
},
() -> System.out.println("UNDEFINED"));
} catch (Exception e) {
System.out.println("ERROR: " + e.getMessage());
}
}
}
}
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
public class FileUtil {
private FileUtil() {
}
public static void writeString(Path path, String content) {
try {
Files.writeString(path, content,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static List<String> readAllLines(Path path) {
try {
return Files.readAllLines(path, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.HexFormat;
public class FileSignatureReader {
private final int byteLimit;
public FileSignatureReader(int byteLimit) {
this.byteLimit = byteLimit;
}
public String readFileSignature(File file) {
try (InputStream is = new FileInputStream(file)) {
return HexFormat.of().formatHex(is.readNBytes(byteLimit))
.toLowerCase();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
public final class SignatureDictionary {
private final Map<String, String> signatureToType;
private final int maxSignatureLength;
public SignatureDictionary(Path resultFile) {
this.signatureToType = FileUtil.readAllLines(resultFile).stream()
.map(String::trim)
.filter(line -> !line.isEmpty() && line.contains(","))
.map(line -> line.split(",", 2))
.collect(Collectors.toMap(
parts -> parts[1].trim().replace(" ", ""),
parts -> parts[0].trim()));
this.maxSignatureLength = signatureToType.keySet().stream()
.mapToInt(String::length)
.max()
.orElseThrow();
}
public int getMaxSize() {
return maxSignatureLength;
}
public Optional<String> getFiletype(String fileSignature) {
return signatureToType.entrySet().stream()
.filter(entry -> fileSignature.startsWith(entry.getKey()))
.map(Map.Entry::getValue)
.findAny();
}
}
Если есть вопросы - спрашивайте, не стесняйтесь Да, я удалил комменты, просто потому, что теперь они не соответсвуют реальнрости
- тут все верно - 8 версия давно устарела, писать на ней странно
- поскольку функциональное программирование появилось в версии 8, то в настоящее время (ведь прошло много времени)уже довольно много кода написано именно таким образом, посему знание функционалки ультимативно, эти скилы нужно прокачивать однозначно
- см.п.2
- см.п.2
- в целом стримы почти всегда будут выигрывать (не забывайте, что они еще и отлично и очень легко паралеляться, что в реальном мире с огромным количеством данных ой как не повредит). вместе с тем есть довольно большое количество интерфейсов, написанных довольно давно, с которыми не получится взаимодействовать через стрим. тут у вас не будет выбора. в целом на первых порах можете писать все, что получается на стримах. прокачаете навык функционалки, это не будет выглядеть как костыль, а со временем будуту чувствовать когда все таки рациональнее цикл
- см.п.8
- это ощибка моей среды разработки(я исправил это когда адаптировал код по версию 8). System.in закрывать не надо
- разница между Supplier и переменной( когда мы говорим об этом в контексте передачи его как аргумент метода) в том, где имнно будет выполнено действие по получению этой переменной. роедставьте себе такую ситуацию. у меня есть метод "а" и 10 методов, которые вызывают метод "а". метода а в аргументах принимает обычный String, а не Supplier. это значит что все 10 методов должны получить String и передать его. а если в 8 из 10 методов возможно исключение? нам нужно 8 раз в разных методах обрабатывать одно и тоже исключение. в случае с Supplier вызов метода, который вернет нужный нам String, произойдет не именно в теле метода "а", поэтому мы пишем обработку исключений 1 раз в методе "а", а не 8 раз в каждом из методов. для нашего конкртеного случая это несущественно, но я хотел бы показать разницу между переменной и лямбдой (в контексте аргумента метода)
- UncheckedIOException это непроверяемое исключение. все дело в том, что проверяемые исключения в целом были признаны как ошибочный шаг. их использование вызывает довольно много проблем. например, попробуйте вызвать методы, пробрасывающий проверяемое исключение, в стриме. вы увидите насколько кривая конструкция получится. посему UncheckedIOException это способ избавиться от костыльного проверяемого исключения IOException. не нужно маниакально менять все проверяемые исключения сразу, ведь далеко не всегда это будет вам мешать. вторая сторона медали - паттерны conroller (grasp) и facade(gof). по сути все сводится к изоляции взаимодействий с "внешним миром" и приведения к удобному интерфейсу. посмотрите на класс FileUtil. я взаимодействую с внешним миром - файловой системой. а внешний мир - это всегда много проблем. если я размажу это взаимодействие по коду, а потом вдруг окажется что файловая система не всегда доступна и нужно реализовывать альтернативную логику на этот случай, то я попегу размазывать эту альтернативную логику по всему коду. вместо этого я сделал класс, отвечающий за такое взаимодействие и далее любые изменения взаимодействия с файловой системой будут влиять тольок на этот класс. кроме того, я одновременно избавляюь от неудобного для меня проверяемого исключения. теперь по поводу "почеиу просто не пробросить наверх". это хороший вопрос и не относится к проверяемым/непроверяемым исключениям никак: и те и другие пробрасываются наврех одинаково, разница только в том, нужно ли их декларировать в сигнатуре метода. а в целом общее правилопо исключениям состоит в том, что мы обрабатываем только то, что можем обработать корртекно. писать catch, который ловит exception и ничего не делает просто , чтобы не падало, это макстсмально плохое решение ибо теперь мы ничего не знаем о проблемах, которые есть, а код работает некорреткно. исправление такого кода займет намного больше времени,чем если бы прилетело обычное ислкючение.
- это самый сложный вопрос , ответ на него придет с опытом. но!!! как вы уже видели, я выше ссылался на паттерны. так вот, принципы и паттерны очень помогут вам получить ответ на этот вопрос. итого, обратите внимание на принципы kiss, dry, yagni, также на паттерны grasp, кроме того, вы наверное слышали про solid, я бы сказал, что он говорит ровно о том же, что и grasp, но grasp, как мне кажется, для понимания проще. когда с этим освоитесь, посмотрите на паттерны gof. это поможет сделать ваш код лучше и критически смотреть на свой же код, не имея под рукой ревьюверов.
Кроме того, основные проблемы вашего кода не в циклах и прочих "точечных" изменениях. Почитайте про декомпозицию предметной области и еще раз обратите внимание на п.10. С этими знаниями начните с проектирования своего кода. программирование не начинается со среды разработки, оно начинается с проектирования, мбо весь код, который вы напишете "на колекне", вы гарантировано перепишите и не один раз. в этом нет смысла. нужно четко понимать какой класс за что отвечает и какие методы этого класса какие задачи выполняют. правильное деление на классы и методы в соответствии с указанными принципами/паттернами - ключ к хорошему коду Используйте ООП и ФП вместо процедурного программирования Избавляйтесть от статики! Это чуть ли не самое большое зло. Статикой должны оставаться утилитные методы (это не значит, что мы теперь делаем утилем вообще все), а также финальные переменные. в вашем коде почти все статическое. Старайтесь не изобретать велосипед. Делать то, что же сделано - задача неблагодарная. Используйте не те классы, которые вы хорошо знаете, потому что вы их хорошо знаете, а те, которые лучше подходят под задачу.