Java 泛型的理解和应用

概述 泛型是一种将类型参数化的动态机制,使用得到的话,可以从以下的方面提升的你的程序: 安全性:使用泛型可以使代码更加安全可靠,因为泛型提供了编译时的类型检查,使得编译器能够在编译阶段捕捉到类型错误。通过在编译时检查类型一致性,可以避免在运行时出现类型转换错误和 ClassCastException 等异常。减少由于类型错误引发的bug。 复用和灵活性:泛型可以使用占位符 <T> 定义抽象和通用的对象,你可以在使用的时候再来决定具体的类型是什么,从而使得代码更具通用性和可重用性。 简化代码,增强可读性:可以减少类型转换的需求,简化代码,可以使代码更加清晰和易于理解。通过使用具有描述性的泛型类型参数,可以更准确地表达代码的意图,还可以避免使用原始类型或Object类型,从而提供更多的类型信息,使代码更加具有表达力 这就是泛型的概念,是 Java 后期的重大变化之一。泛型实现了参数化类型,可以适用于多种类型。泛型为 Java 的动态类型机制提供很好的补充,但是 Java 的泛型本质上是一种高级语法糖,也存在类型擦除导致的信息丢失等多种缺点,我们可以在本篇文章中深度探讨和分析。 简单的示例 泛型在 Java 的主要作用就是创建类型通用的集合类,我们创建一个容器类,然后通过三个示例来展示泛型的使用: 没有使用泛型的情况 使用 Object 类型作为容器对象 使用泛型作为容器对象 示例1:没有使用泛型的情况 public class IntList { private int[] arr; // 只能存储整数类型的数据 private int size; public IntList() { arr = new int[10]; size = 0; } public void add(int value) { arr[size++] = value; } public int get(int index) { return arr[index]; } public int size() { return size; } public static void main(String[] args) { IntList list = new IntList(); list.add(1); list.add(2); list.add(3); int value = list.get(1); // 需要显式进行类型转换 System.out.println(value); // 输出: 2 } } 在上述示例中,使用了一个明确的 int 类型存储整数的列表类 IntList,但是该类只能存储整数类型的数据。如果想要存储其他类型的数据,就需要编写类似的类,导致类的复用度较低。 ...

May 24, 2023 · 6 min

Java 反射的理解和应用

概述 反射(Reflection)机制是指在运行时动态地获取类的信息以及操作类的成员(字段、方法、构造函数等)的能力。通过反射,我们可以在编译时期未知具体类型的情况下,通过运行时的动态查找和调用。 虽然 Java 是静态的编译型语言,但是反射特性的加入,提供一种直接操作对象外的另一种方式,让 Java 具备的一些灵活性和动态性,我们可以通过本篇文章来详细了解它 为什么需要反射 ? Java 需要用到反射的主要原因包括以下几点: 运行时动态加载,创建类:Java中的类是在编译时加载的,但有时希望在运行时根据某些条件来动态加载和创建所需要类。反射就提供这种能力,这样的能力让程序可以更加的灵活,动态 动态的方法调用:根据反射获取的类和对象,动态调用类中的方法,这对于一些类增强框架(例如 Spring 的 AOP),还有安全框架(方法调用前进行权限验证),还有在业务代码中注入一些通用的业务逻辑(例如一些日志,等,动态调用的能力都非常有用 获取类的信息:通过反射,可以获取类的各种信息,如类名、父类、接口、字段、方法等。这使得我们可以在运行时检查类的属性和方法,并根据需要进行操作 一段示例代码 以下是一个简单的代码示例,展示基本的反射操作: import java.lang.reflect.Method; public class ReflectionExample { public static void main(String[] args) { // 假设在运行时需要调用某个类的方法,但该类在编译时未知 String className = "com.example.MyClass"; try { // 使用反射动态加载类 Class<?> clazz = Class.forName(className); // 使用反射获取指定方法 Method method = clazz.getMethod("myMethod"); // 使用反射创建对象 Object obj = clazz.newInstance(); // 使用反射调用方法 method.invoke(obj); } catch (ClassNotFoundException e) { System.out.println("类未找到:" + className); } catch (NoSuchMethodException e) { System.out.println("方法未找到"); } catch (IllegalAccessException | InstantiationException e) { System.out.println("无法实例化对象"); } catch (Exception e) { System.out.println("其他异常:" + e.getMessage()); } } } 在这个示例中,我们假设在编译时并不知道具体的类名和方法名,但在运行时需要根据动态情况来加载类、创建对象并调用方法。使用反射机制,我们可以通过字符串形式传递类名,使用 Class.forName() 动态加载类。然后,通过 getMethod() 方法获取指定的方法对象,使用 newInstance() 创建类的实例,最后通过 invoke() 方法调用方法。 ...

May 21, 2023 · 4 min

Java.nio.file 文件库的介绍和使用

概述 在早期的 Java 版本中,文件 IO 操作功能一直相对较弱,主要存在以下问题: 缺乏对现代文件系统的支持:只提供的基础的文件操作,不支持很多现代的文件系统 API 不够直观:文件操作的 API 设计相对较为复杂和冗长,使用体验感很差 对于大文件处理和并发性能不够:简单的 I/O 模型,没有充分利用现代硬件的性能优势,而且还有很多同步的问题 但 Java 在后期版本中引入了 java.nio.file 库来提高 Java 对文件操作的能力。还增加的流的功能,似乎使得文件变成更好用了。所以本章,我们就来主要介绍 java.nio.file 中常用的类和模块,大致如下: Path 路径:Paths 模块和 Path 工具类介绍 Files 文件:File 和 FileSystems 工具类介绍 文件管理服务:WatchService 、PathMatcher 等等文件服务 Path 路径 java.nio.file.Paths 和 java.nio.file.Path 类在 Java NIO 文件 I/O 框架中用于处理文件系统路径。以下是对它们的简单介绍: Paths 模块:Paths 模块提供了一些静态方法来创建 Path 对象,Path 对象表示文件系统中的路径。例如,可以使用 Paths.get() 方法创建一个 Path 对象,这个对象表示一个文件路径。 Path 类:Path 类代表一个文件系统中的路径,它提供了一系列的方法来操作文件路径。例如,可以使用 Path.toAbsolutePath() 方法获取一个绝对路径,或者使用 Path.getParent() 方法获取路径的父路径。 关于跨平台:Path 对象可以工作在不同操作系统的不同文件系统之上,它帮我们屏蔽了操作系统之间的差异 以下是一些简单使用场景示例: import java.nio.file.Path; import java.nio.file.Paths; public class PathExample { public static void main(String[] args) { // 创建一个绝对路径 Path absolutePath = Paths.get("C:\Users\phoenix\file.txt"); // 这里传入 "example\file.txt" 创建的相对路径 System.out.println("Absolute path: " + absolutePath); // 获取父路径 System.out.println("Parent path: " + absolutePath.getParent()); // 获取文件名 System.out.println("File name: " + absolutePath.getFileName()); // 获取根路径 System.out.println("Root path: " + absolutePath.getRoot()); // 合并路径 Path resolvePath = Paths.get("C:\Users\phoenix").resolve("file.txt"); System.out.println("Merged path:" + resolvePath); } } 输出结果: ...

May 9, 2023 · 4 min

提高代码质量的几条参考建议

单元测试 什么是单元测试 ? 单元测试通常是指对一个函数或方法测试。单元测试的目的是验证每个单元的行为是否符合预期,并且在修改代码时能够快速检测到任何潜在的问题。通过编写测试用例,我们可以验证这些模块在特定输入下是否产生正确的输出。单元测试的目的是确保每个模块在各种情况下都能正常运行。 写单元测试的好处 可以带来以下几个好处: 提高代码质量:单元测试可以我们提前的发现代码中的潜在问题,例如边界条件、异常情况等,从而减少出错的概率。 提高代码可维护性:单元测试可以帮助开发人员理解代码的功能和实现细节,从而更容易维护和修改代码。 提高代码可靠性:修改代码后,可以通过单元测试可以帮助开发人员验证代码的正确性,从而提高代码的可靠性。 写单元测试是一种良好的软件开发实践,可以提高代码质量、可维护性和可靠性,同时也可以提高开发效率和支持持续集成和持续交付。 单元测试入门 上手单元测试,通常同时从静态测试(Static Test)开始,因为它简单,好理解,静态测试(Static Test)是指在编写测试用例时,我们提前定义好所有的测试方法和测试数据。这些测试方法和数据在编译时就已经确定,不会在运行时发生变化。Junit 中的静态测试通常的常规注解,如 @Test、@Before、@After 等。先来看看一组简单的静态测试示例。 首先,确保你的 pom.xml 文件包含 JUnit 的依赖: <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.8.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.8.0</version> <scope>test</scope> </dependency> </dependencies> 然后,创建一个简单的计算器类,通常这里替换为你实际要测试的业务类: public class SimpleCalculator { public int add(int a, int b) { return a + b; } public int subtract(int a, int b) { return a - b; } } 然后在 /test 的相同目录下创建对应的测试类 import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.assertEquals; public class SimpleCalculatorTest { // 在所有测试方法执行前,仅执行一次。这个方法需要是静态的。 @BeforeAll static void setup() { System.out.println("BeforeAll - 初始化共享资源,例如数据库连接"); } // 在所有测试方法执行后,仅执行一次。这个方法需要是静态的。 @AfterAll static void tearDown() { System.out.println("AfterAll - 清理共享资源,例如关闭数据库连接"); } // 在每个测试方法执行前,都会执行一次。用于设置测试方法所需的初始状态。 @BeforeEach void init() { System.out.println("BeforeEach - 初始化测试实例所需的数据"); } // 在每个测试方法执行后,都会执行一次。用于清理测试方法使用的资源。 @AfterEach void cleanup() { System.out.println("AfterEach - 清理测试实例所用到的资源"); } // 标注一个测试方法,用于测试某个功能。 @Test void testAddition() { System.out.println("Test - 测试加法功能"); SimpleCalculator calculator = new SimpleCalculator(); assertEquals(5, calculator.add(2, 3), "2 + 3 应该等于 5"); } // 再添加一个测试方法 @Test void testSubtraction() { System.out.println("Test - 测试减法功能"); SimpleCalculator calculator = new SimpleCalculator(); assertEquals(1, calculator.subtract(3, 2), "3 - 2 应该等于 1"); } } 以上程序,可以看到 Junit 常用注解使用说明: ...

April 25, 2023 · 4 min

Java 异常处理的使用和思考

概念 异常处理的概念起源于早期的编程语言,如 LISP、PL/I 和 CLU。这些编程语言首次引入了异常处理机制,以便在程序执行过程中检测和处理错误情况。异常处理机制随后在 Ada、Modula-3、C++、Python、Java 等编程语言中得到了广泛采用和发展。在 Java 中,异常处理是提供一种在程序运行时处理错误和异常情况的方法。异常处理机制使得程序能够在遇到错误时继续执行,而不是立即崩溃。这种机制使程序更具有健壮性和容错性。异常分为两类:受检异常(Checked Exceptions)和非受检异常(Unchecked Exceptions) 受检异常(Checked Exceptions): 受检异常是指那些在编译时必须处理的异常。它们通常是由程序员的错误或外部资源问题引起的。例如,IOException、FileNotFoundException 等。受检异常必须在方法签名中使用 throws 关键字声明,或者在方法体内用 try-catch 块捕获和处理。 非受检异常(Unchecked Exceptions): 非受检异常是指那些在编译时不强制要求处理的异常。它们通常是由编程错误引起的,如空指针异常(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。非受检异常继承自 java.lang.RuntimeException 类,不需要在方法签名中声明,也不需要强制捕获和处理。 它们的关系如下: 异常处理 Java 使用 try/catch 关键字进行异常捕获,使用 throw 声明抛出异常,示例代码如下: public class NullPointerExceptionExample { public static void main(String[] args) { String nullString = null; try { int length = nullString.length(); } catch (NullPointerException e) { System.out.println("Caught NullPointerException!"); e.printStackTrace(); } } } 在这个示例中,我们尝试获取一个 null 字符串的长度。当调用 nullString.length() 时,会抛出 NullPointerException。我们使用 try-catch 语句捕获异常并处理它 自定义异常 Java 官方异常不可能预见所有可能发生的错误,有时候你需要结合自己的业务场景,例如以下场景: ...

April 9, 2023 · 6 min

Java Stream 流式编程的介绍

概述 Stream API 是 Java 中引入的一种新的数据处理方法。它提供了一种高效且易于使用的方法来处理数据集合。Stream API 支持函数式编程,可以让我们以简洁、优雅的方式进行数据操作,还有使用 Stream 的两大原因: 在大多数情况下,将对象存储在集合中就是为了处理它们,因此你会发现你把编程 的主要焦点从集合转移到了流上。 当 Lambda 表达式和方法引用(method references),流(Stream)结合使用的时候会让人感觉自成一体,行云流水的感觉 先展示一段简单的流式编程: import java.util.Random; public class Randoms { public static void main(String[] args) { // 随机展示 5 至 20 之间不重复的整数并进行排序 new Random(47) .ints(5, 20) .distinct() // 使流中的整数不重复 .limit(7) // 获取前 7 个元素 .sorted() // 排序 .forEach(System.out::println); } } 输出结果: 6 10 13 16 17 18 19 实际上函数式的编程风格是声明式(Declarative programming)的,它声明了要做什么, 而不是指明(每一步)如何做。 相同的程序,相比声明式风格,命令式(Imperative)编程的形式(指明每一步如何做),代码阅读起来会更难理解: import java.util.Random; import java.util.SortedSet; import java.util.TreeSet; public class ImperativeRandoms { public static void main(String[] args) { Random rand = new Random(47); SortedSet<Integer> rints = new TreeSet<>(); while (rints.size() < 7) { int r = rand.nextInt(20); if (r < 5) continue; rints.add(r); } System.out.println(rints); } } 输出结果: ...

April 6, 2023 · 12 min

基于 Spring Cloud 的微服务设计原则

最近几年微服务很火,大家都在建设微服务,如果不懂点微服务相关的技术,都不好意思跟同行打招呼了,也见过身边很多人在微服务踩过很多坑,我从 16 年开始接触微服务,有多家大型企业的微服务分布式系统的架构经验,所以就打算跟大家做一期关于微服务的分享,不过微服务和涉及的分布式计算非常的复杂,绝非是一篇文章就可以讲清楚的,本文只是从最简单的概念的基本使用带你入门,如果后续还有兴趣的话,可以查阅相关的文献和技术书籍去深入学习 本文的微服务分享以下三部分,总体大纲如下: 微服务的概念和原则(理论) Spring Cloud 如何低成本的实现微服务(实现) Spring Cloud 大型项目的架构方案(真实案例) 微服务的概念和原则 什么是微服务? 简单举例:看军事新闻的同学应该都知道,一艘航空母舰作战能力虽然很强,但是弱点太明显,就是防御能力太差,单艘的航空母舰很少单独行动,通常航空母舰战斗群才是主要军事力量,你可以把单艘航母理解为的单体应用(防御差,机动性不好),把航母战斗群(调度复杂,维护费用高)理解为微服务。 简单讲特征就是: 单体应用:简单,脆弱(某个模块出问题,整个系统不可用),战斗力弱,维护成本低 微服务架构:复杂,健壮(某个模块出问题,不会影响系统整体的可用性),战斗力强,维护成本高 大部分的开发者经历和开发过单体应用,无论是传统的 SSM,还是现在的 SpringBoot/Rails,它们都是单体应用,那么长期陪伴我们的单体应用有什么弊端?我们是面临了什么问题,导致我们要抛弃单体应用转向微服务架构?个人总结主要问题如下: 部署成本高(无论是修改1行代码,还是10行代码,都要全量部署替换) 改动影响大,风险高,测试成本高(不论代码改动多小,成本都相同) 因为成本高,风险高,所以导致部署频率低(无法满足快速交付客户需求) 当然还有例如无法满足快速扩容,弹性伸缩,无法适应云环境特性等问题但我们不一一详谈了,以上的问题,都是微服务架构要解决的问题,至于具体是怎么解决的,我们先放到后面再聊 解决什么问题,又引入了什么问题? 我们先看看微服务能带给我们什么?微服务架构的特点: 针对特定服务发布,影响小,风险小,成本低 频繁发布版本,快速交付需求 低成本扩容,弹性伸缩,适应云环境 我们知道一个朴素的理念,没有任何事物是完美的,任何东西都有两面性,有得必有失那么在选择微服务在解决了快速响应和弹性伸缩的问题同时,它又给我们带来了什么问题?个人总结如下: 分布式系统的复杂性 部署,测试和监控的成本问题 分布式事务和 CAP 的相关问题 系统应用由原来的单体变成几十到几百个不同的工程,会所产生例如包括服务间的依赖,服务如何拆封,内部接口规范,数据传递等等问题,尤其是服务拆分,需要团队熟悉业务流程,懂得取舍,要保证拆分的粒度服务既符合“高内聚,低耦合”的基本原则,还要兼顾业务的发展以及公司的愿景,要还要说服团队成员为之努力,并且积极投入,在多方中间取得平衡。 对于分布式系统,部署,测试和监控都需要大量的中间件来支撑,而且中间件本身也要维护,原先单体应用很简单的事务问题 ,转到分布式环境就变得很复杂,分布式事务是采用简单的重试+补偿机制,还是采用二阶段提交协议等强一致性方法来解决,就要取决对业务场景的熟悉加上反复的权衡了,相同问题还包括对 CAP 模型的权衡,总之微服务对团队整体的技术栈水平整体要求更高 微服务有应该遵循哪些原则? 古人云:兵马未动,粮草先行。建设微服务是需要建立长远规划,不是像写 CMS 那样建好数据库表,然后就开始干活,这样十有八九是会失败的。我们要进行微服务改造前,架构师要提前做好规划,我们把这里分为三步,前期阶段,设计阶段,技术阶段 前期阶段,大致要做好如下事情: 和多方充分沟通,确保能符合客户和组织的需求,并且得到认同 和团队沟通,让队友(开发/测试/运维)理解,并且积极投入 和业务部门沟通,指定版本计划和上线时间 设计阶段,参考 Sam Newman 的著作《微服务设计》,单微服务必须要满足以下的条件,才符合微服务的基本要求: 标准的 REST 风格接口(基于 HTTP 和 JSON 格式) 独立部署,避免共享数据库(避免因为数据库而影响整个分布式系统) 业务上的高内聚,减少依赖(从设计上要避免服务过大或者太小) 庞大的分布式系统,需要强大基础设施来支撑,微服务涉及哪些基础设施? CI/CD和自动化(分布式系统几乎不可能通过人工手动发布) 虚拟化技术(要保证微服务运行环境隔离,目前行业主流的是使用 Docker 容器) 日志聚合,全链路监控(高度可观察和分析诊断问题) 说了那么多,那什么样的情况下,你的团队不适合建设微服务?(请勿对号入座) 开发团队不具备自主性,所在组织对开发团队限制非常多(具体请参考 康威定律) 团队不熟悉业务,无法识别出服务的边界,进行合理的拆分(请参考 DDD 领域驱动设计) 微服务设计其实是很早就有的设计思想,因为随着虚拟化技术的崛起,微服务可以低成本的实现,所以也开始流行和兴起。 ...

September 14, 2020 · 3 min