Java编码思考录-日常开发中的写法抉择 本文记录日常 Java 项目开发中的编码思考与方案取舍案例均来自各类真实项目实践。文中结合实际场景探讨代码命名、语法实现、代码结构设计等常见问题对比不同写法的优劣与适用场景。本内容为连载形式后续会持续补充更多编码话题、实战经验与优化思路逐步丰富内容体系。碍于时间文中代码仅用来辅助表达想法全部是日常项目里的原始代码没有专门规整。大家不必纠结细节重点参考思路即可其中瑕疵还请见谅。1. 命名想一个好名字非常复杂有的时候是根本没能力做到我自己在看到AI起的名字后深感差距缺乏的包括英文单词的准确意思行业用语需要太多的文化背景至少我们应该具备鉴赏能力今天推荐和AI配合来选择好名字你需要提供想法和鉴赏力。下面用我写的一个文件处理工具来看下AI想的名字和我想的名字的差别有多大需求背景对比两个文件夹找出相同不相同独自存在的文件。以及一个文件夹内重复文件等。表示相同我是Same通俗易懂。AI建议Duplicate表示单个目录内的重复带有副本之意Identical表示两个目录中重复表示完全相同。表示只在一边有我是Alone。AI建议Unique简写Uniq行业统一术语。表示修改的我能想到的是Modified或ModificationAI建议Delta原来数学里表示变量后来计算机表示变动的。看到了吧起个名字不是个技术活需要了解很多背景知识才可以而这是AI擅长的我们可以指挥AI来完成AI知道足够的背景知识需要我们正确的指挥AI告诉意图根据反馈提供改进的思路。核心的是鉴赏能力背景信息AI来补齐。2. 枚举枚举不仅仅是几个枚举值如下代码通过传入的枚举来计算文件的摘要MD5或者文件长度。/** * 原始写法逻辑写在外部通过枚举类型判断 */ public static String calculate(File file, DigestType type) { switch (type) { case MD5: // 外部调用MD5工具方法 return FileUtil.getMD5(file); case LEN: // 外部获取文件长度 return String.valueOf(file.length()); default: throw new IllegalArgumentException(不支持的摘要类型); } }优化一可以加入到枚举中提升内聚性。public enum DigestType { MD5, LEN; public String calculate(File file) { // this 当前的枚举值MD5 或 LEN return switch (this) { case MD5 - FileUtil.getMD5(file); case LEN - String.valueOf(file.length()); }; } }优化二枚举 特殊的 Java 类MD5/LEN是这个类的子类实例每个枚举值支持单独重写抽象方法。对比优化一父类写 1 个方法大家共用。public enum DigestType { MD5 { Override public String calculate(File file) { return FileUtil.getMD5(file); } }, LEN { Override public String calculate(File file) { return String.valueOf(file.length()); } }; public abstract String calculate(File file); }3. 重载方法和参数太多比如如下不用太细看明白主要几个意图就可以提供File和String两种参数方便调用方使用传文件或文件路径都可以。返回值也一样可以支持File和StringFileFilter,支持自定义一些过滤。public static LinkedHashMapFile, String getDirDigestFileMap(File checkBaseDir, DigestType digestType, PredicateFile noNeedCalMd5FileFilter) ; public static LinkedHashMapFile, String getDirDigestFileMap(String checkBaseDirStr, DigestType digestType, PredicateFile noNeedCalMd5FileFilter) ; public static LinkedHashMapString, String getDirDigestMap(String checkBaseDirStr, DigestType digestType, PredicateFile noNeedCalMd5FileFilter) ; 。。。。方案一把部分传入做成属性不同的使用场景使用Spring为不同实例配置不同的属性值这样就不用传入。这样太局限了只有部分属性可以使用我未采用但此方法在很多场景适用比如不同的数据库连接配置不同的连接实例。bean 1 property namexxxfilter1/property /bean 1 bean 2 property namexxxfilter1/property /bean 2方案二参数对象化使用Builder方便的创建参数对象。原来的调用// 1. 计算目录MD5默认规则 DigestCalculate.getDirDigestMap(/User/test, DigestCalculate.DigestType.MD5, FxConst.fileAndDirPredicate); // 2. 计算文件长度 DigestCalculate.getDirDigestMap(/User/test, DigestCalculate.DigestType.LEN, FxConst.fileAndDirPredicate); // 3. File对象入参调用重载方法 DigestCalculate.getDirDigestFileMap(new File(/User/test), DigestCalculate.DigestType.MD5, FxConst.fileAndDirPredicate);新的使用Builder// 1. 默认使用项目目录过滤 MD5最常用 DigestConfig config1 DigestConfig.builder() .baseDir(/User/test) .digestType(DigestCalculate.DigestType.MD5) .build(); DigestCalculate.getDirDigestMap(config1); // 2. 仅过滤Mac文件 文件长度 DigestConfig config2 DigestConfig.builder() .baseDir(new File(/User/test)) .digestType(DigestCalculate.DigestType.LEN) .fileFilter(DigestConfig.FILTER_MAC) .build(); // 3. 不过滤任何文件 DigestConfig config3 DigestConfig.builder() .baseDir(/User/test) .digestType(DigestCalculate.DigestType.MD5) .fileFilter(DigestConfig.FILTER_NONE) .build();Builder 类有点长我删除掉一些能看出意思就行了//文件摘要计算配置不可变对象线程安全 Builder 模式内置常用过滤规则 public class DigestConfig { private final File baseDir; private final DigestCalculate.DigestType digestType; private final PredicateFile fileFilter; // 项目默认过滤.idea/target/.git/.DS_Store 目录 iml后缀 public static final PredicateFile FILTER_PROJECT new FileAndDirPredicate( Set.of(.idea, target, .DS_Store, .git), Set.of(iml) ); // 仅过滤Mac系统文件 public static final PredicateFile FILTER_MAC new FileAndDirPredicate( Set.of(.DS_Store), Set.of() ); // 不过滤任何文件 public static final PredicateFile FILTER_NONE file - false; // 私有构造仅Builder可创建 private DigestConfig(Builder builder) { this.baseDir builder.baseDir; this.digestType builder.digestType; this.fileFilter builder.fileFilter; } // ------------------- Guava 风格 Builder ------------------- public static class Builder { private File baseDir; private DigestCalculate.DigestType digestType; private PredicateFile fileFilter FILTER_PROJECT; // 默认用项目过滤 // 设置目录支持 String/File 两种入参 public Builder baseDir(String dirPath) { this.baseDir new File(dirPath); return this; } public Builder baseDir(File baseDir) { this.baseDir baseDir; return this; } public Builder digestType(DigestCalculate.DigestType digestType) { this.digestType digestType; return this; } public Builder fileFilter(PredicateFile fileFilter) { this.fileFilter fileFilter; return this; } public DigestConfig build() { // 必填参数校验 if (null baseDir) { throw new IllegalArgumentException(目录不能为空); } if (null digestType) { throw new IllegalArgumentException(摘要类型不能为空); } return new DigestConfig(this); } } public static Builder builder() { return new Builder(); } }4. 流式处理Stream的误区Java的流式处理有一个误区我也开始也想当然认为是整体执行完一步然后下一步其实并不是说起来比较抽象看代码吧比如我要遍历文件系统的markdown文章然后过滤文章。getCplArticleStream(queryParam.getPin(), path) .filter(article - articleCount.incrementAndGet() MAX_ARTICLES) .map(cplArticle - exeFilterArticleSPI(cplArticle, queryParam)) .filter(Objects::nonNull) .forEach(filteredArticles::add);真实的执行是这样stream .filter(计数) // 第1条数据进来 .map(处理) // 第1条继续走 .filter(非空判断) // 第1条继续走 .forEach(添加) // 第1条走完 // 然后才轮到第2条数据从头走一遍流水线原来我想当然以为是这样的所有数据先全部filter → 再全部map → 再全部filter其实是这样的文章1 → 计数 → 判断 → 处理 文章2 → 计数 → 判断 → 处理 文章3 → 计数 → 判断 → 处理 # 或者这样看更方便 文章1 → 计数filter → map处理 → 非空filter → 加入列表 文章2 → 计数filter → map处理 → 非空filter → 加入列表中间如果有filter()函数是当前数据处理完毕接着处理下一条。但有一些中断是退出循环后续的根本不执行。比如在我的例子中我希望取出固定条数到了条数退出循环剩下的文件根本不会遍历访问节省效率。上面的例子故意写错了其实并不会中断需要改为中断的函数.limit(MAX_ARTICLES) // 核心取满MAX条直接终止流提示下Stream 里只有这些操作能真正终止遍历、不再处理后续数据1. limit (数量)取够指定条数就停2. anyMatch /allMatch/noneMatch条件满足就停3. findFirst /findAny找到数据就停像 filter 这种只是过滤数据不会停流。本文目的并不是说明Stream详细语法理解了Stream的使用想法记住有这么回事用的时候注意就可以。5. Map数据的可读性日常业务开发里经常会用到各种 Map还有 Map 嵌套 List 这类复杂的数据结构。这种代码要是写得不好不仅特别难读懂还容易出错。我就不展示差的写法了直接分享我觉得好用的写法。首先应该把Map处理逻辑和业务逻辑拆分开业务逻辑不应该见到复杂的Map的各种转换等通用的非业务逻辑。例如程序中需要把key和value做一个转换我可以把这个过程抽象到一个公共的技术组件中示例代码如下public static K, V LinkedHashMapV, ListK convert2ValueMap(MapK, V map) { LinkedHashMapV, ListK valueMap new LinkedHashMap(); map.forEach((k, v) - { ListK keys valueMap.computeIfAbsent(v, key - new ArrayList()); keys.add(k); }); return valueMap; }业务代码中只要如下使用即可//转换MapKey:文件长度value长度相等的文件列表 LinkedHashMapString, ListFile lengthGroupFileMap MapUtil.convert2ValueMap(fileLengthMap);其次Map命名采用XXX_key的形式这样可以见名指意思比如我会采用下面的命名方式只用关心Map的含义方便阅读就好很多做Java的认为这种命名好像不规范用过就知道特别好用能非常好的解决Map的易读性//不用注释也可以清楚知道key是文件md5value是文件列表 LinkedHashMapString, ListFile leftGroupFileMap_md5 LinkedHashMapUtil.convert2ValueMap(leftMap_File); LinkedHashMapString, ListFile rightGroupFileMap_md5 LinkedHashMapUtil.convert2ValueMap(..); //不用注释也可以清楚知道key是文件长度value是文件列表 LinkedHashMapString, ListFile lengthGroupFileMap_len LinkedHashMapUtil.convert2ValueMap(..);**最后对复杂数据结构进行封装。**特别复杂只能把复杂封装起来但记住封装也有成本封装和_key命名方式并不冲突两种都是我很喜欢的方式大家可以根据情况选取都挺好用。当然大部分Java开发都认为这种封装更好我认为没有最好的技术只有符合当前场景的技术对MapString,List的封装示例代码如下。public class MultimapV { private final MapString, ListV map new HashMap(); public void put(String key, V value) { map.computeIfAbsent(key, k - new ArrayList()).add(value); } public ListV get(String key) { return map.getOrDefault(key, new ArrayList()); } public MapString, ListV asMap() { return map; } }使用的时候如下// 添加的时候一个个添加像一个普通的Map MultimapFile multimap new Multimap(); multimap.put(md5, file1); multimap.put(md5, file2); // 获取的时候可以文件列表 ListFile files multimap.get(md5);6. Properties文件编码JDK9的变化Java 9 以前默认是 ISO-8859-1 编码汉字自动转 Unicode 编码读取的时候会自动转回来。比如你写name张三到配置文件文件里会自动变成这样name\u5f20\u4e09取的时候会自动转码Properties prop new Properties(); // 加载文件JDK8默认ISO-8859-1 prop.load(new FileInputStream(config.properties)); // 写入文件JDK8中文自动转 \u 转义字符 prop.setProperty(name, 张三); prop.store(new FileOutputStream(config.properties), test);上面的大家都知道但JDK9之后开始原生支持UTF-8。有个小坑IDEA 默认会把 properties 文件编码设为 ISO-8859-1需要我们手动改成 UTF-8后续版本可能放开限制。JDK9 和 JDK8 核心区别JDK9 新增了直接加载文件的方法默认 UTF-8不用再手动包装流。Properties p new Properties(); // JDK8写法必须套 InputStreamReader 才能UTF-8 p.load(new InputStreamReader(new FileInputStream(a.properties), StandardCharsets.UTF_8)); // JDK9写法直接传路径默认UTF-8 p.load(Paths.get(a.properties));注意JDK9 原有load(流)旧方法依旧沿用 ISO-8859-1 编码只有新增的传文件路径重载方法默认使用 UTF-8。