Java程序员必需掌握的 4 大基础
作者:Himanshu Verma
原文:https://medium.com/swlh/4-things-that-java-developer-thinks-are-most-confusing-complicated-87c2598f33f0
译者:弯月,责编:屠敏,出品:CSDN(ID:CSDNnews)
大概每个人在学生时代开始就使用Java了,我们一直在学习Java,但Java中总有一些概念含混不清,不论是对初级还是高级程序员都是如此。所以,这篇文章的目的就是弄清楚这些概念。
读完本文你会对这些概念有更深入的了解,还能弄清楚一切灰色的东西。在本书中,我们将讨论匿名内联类、多线程、同步和序列化。
1
匿名类
Java匿名类很像局部类或内联类,只是没有名字。我们可以利用匿名类,同时定义并实例化一个类。只有局部类仅被使用一次时才应该这么做。
匿名类不能有显式定义的构造函数。相反,每个匿名类都隐含地定义了一个匿名构造函数。
创建匿名类有两种方法:
- 扩展已有的类(可以是抽象类,也可以是具体类)
- 创建接口
理解代码的最好方法就是先阅读,所以我们首先来看看代码。
匿名类可以在类和函数代码块中创建。你也许知道,匿名类可以用接口来创建,也可以通过扩展抽象或具体的类来创建。上例中我先创建了一个接口Football,然后在类的作用域和main()方法内实现了匿名类。Football也可以是抽象类,也可以是与interface并列的顶层类。
Football可以是抽象类,请看下面的代码。
匿名类不仅可以是抽象类,还可以是具体类。
如果Football类没有不带参数的构造方法怎么办?我们可以在匿名类中访问类变量吗?我们需要在匿名类中重载所有方法吗?
匿名类的用途:
- 更清晰的项目结构:通常我们在需要随时改变某个类的某些方法的实现时使用匿名类。这样做就不需要在项目中添加新的*.java文件来定义顶层类了。特别是在顶层类只被使用一次时,这种方法非常好用。
- UI事件监听器:在图形界面的应用程序中,匿名类最常见的用途就是创建各种事件处理器。例如,下述代码:
我们创建了一个匿名类,实现了setOnClickListener接口。当用户点击按钮时会触发它的onClick方法。
2
多线程
Java中的多线程能够同时执行多个线程。线程是轻量级的子进程,也是处理的最小单位。使用多线程的主要目的是最大化CPU的使用率。我们使用多线程而不是多进程,因为线程更轻量化,也可以共享同一个进程内的内存空间。多线程用来实现多任务。
线程的生命周期
如上图所示,线程的生命周期主要有5个状态。我们来依次解释每个状态。
- New:创建线程的实例后,它会进入new状态,这是第一个状态,但线程还没有准备好运行。
- Runanble:调用线程类的start()方法,状态就会从new变成Runnable,意味着线程可以运行了,但实际上什么时候开始运行,取决于Java线程调度器,因为调度器可能在忙着执行其他线程。线程调度器会以FIFO(先进先出)的方式从线程池中挑选一个线程。
- Blocked:有很多情况会导致线程变成blocked状态,如等待I/O操作、等待网络连接等。此外,优先级较高的线程可以将当前运行的线程变成blocked状态。
- Waiting:线程可以调用wait()进入waiting状态。当其他线程调用notify()时,它将回到runnable状态。
- Terminated:start()方法退出时,线程进入terminated状态。
为什么使用多线程?
使用线程可以让Java应用程序同时做多件事情,从而加快运行速度。用技术术语来说,线程可以帮你在Java程序中实现并行操作。由于现代CPU非常快,还可能包含多个核心,因此仅有一个线程就没办法使用所有的核心。
需要记住的要点
- 多线程可以更好地利用CPU。
- 提高响应性,提高用户体验
- 减少响应时间
- 同时为多个客户端提供服务
创建线程的方法主要有两种:
- 扩展Thread类
- 实现Runnable接口
通过扩展Thread类来创建线程
创建一个类扩展Thread类。该类应当重载Thread类中的run()方法。线程在run()方法中开始生命周期。我们创建新类的对象,然后调用start()方法开始执行线程。在Thread对象中,start()会调用run()。
也可以通过接口创建类。
下面的代码创建了一个类,实现java.lang.Runnable接口并重载了run()方法。然后我们实例化一个Thread对象,调用该对象的start()方法。
Thread类与Runnable接口
- 扩展Thread类,就无法扩展更多的类,因为Java不允许多重继承。多重继承可以通过接口实现。所以最好是使用接口而不是Thread类。
- 如果扩展Thread类,那么它还包含了一些方法,如yield()、interrupt()等,我们的程序可能用不到。而在Runnable接口中就没有这些排不上用场的方法。
3
同步
同步指的是多线程的同步。的代码块在同一时刻只能被一个线程执行。Java中的同步是个很重要的概念,因为Java是多线程语言,多个线程可以并行执行。在多线程环境中,Java对象的同步,或者说Java类的同步非常重要。
为什么要同步?
如果代码在多线程环境下执行,那么在多个线程享的对象之间需要同步,以避免破坏状态,或者造成任何不可预料的行为。
在深入同步的概念之前先来理解一下这个问题。
运行这段代码就会注意到,输出结果非常不稳定,因为没有同步。我们来看看程序的输出。
输出:
给printTable()方法加上,那么的方法在执行结束之前不会让其他线程进入。下面的输出结果就非常稳定了。
输出:
类似地,Java的类和对象也可以同步。
注意:我们并不一定需要同步整个方法。有时候最好是仅同步方法的一小部分。Java的代码段可以实现这一点。
4
序列化
Java中的序列化是一种机制,可以将对象的状态写入到字节流中。相反的操作叫做反序列化,将字节流转换成对象。
序列化和反序列化的过程是平台无关的,也就是说,在一个平台上序列化对象,然后可以在另一个平台上反序列化。
序列化时调用ObjectOutputStream的writeObject()方法,反序列化调用ObjectInputStream类的readObject()方法。
下图中,Java对象被转换成字节流,然后存储在各种形式的存储中,这个过程叫做序列化。图右侧,内存中的字节流转换成Java对象,这个过程叫作反序列化。
为什么要序列化
显然,创建的Java类在程序执行结束或中止后,对象就销毁了。为了避免这个问题,Java提供了序列化功能,通过它可以将对象存储起来,或者将状态进行持久化,以便稍后使用,或者在其他平台上使用。
下面的代码演示了该过程。
输出:
输出:
需要记住的重点
- 如果父类实现了Serializable接口,那么子类就不需要实现了,但反过来不一定成立。
- 只有非静态数据成员可以在序列化过程中保存下来。
- 静态数据成员和临时数据成员不会在序列化过程中保存下来。所以,如果不想保存某个非静态数据成员,则可以将其设置为transient。
- 反序列化过程中不会调用对象的构造函数。
- 关联对象必须实现Serializable接口。
5
总结
1、首先我们解释了匿名类,以及用途和使用方法。
2、其次我们讨论了Java中的多线程,线程的生命周期,以及用途。
3、同步只允许一个线程进入同步的方法或代码块去访问资源,其他线程必须在队列中等待。
4、序列化就是存储对象状态供以后使用的过程。
听说你还不知道Java代码是怎么运行的?
作者:Jay_huaxiao
作为一名Java程序员,我们需要知道Java代码是怎么运行的。最近复习了深入理解Java虚拟机这本书,做了一下笔记,希望对大家有帮助,如果有不正确的地方,欢迎提出,感激不尽。
java 代码运行主要流程
本文主要讲解流程如下:
- java源文件编译为class字节码
- 类加载器把字节码加载到虚拟机的方法区。
- 运行时创建对象
- 方法调用,执行引擎解释为机器码
- CPU执行指令
- 多线程切换上下文
编译
我们都知道,java代码是运行在Java虚拟机上的。但是java是一门面向对象的高级语言,它不仅语法非常复杂,抽象程度也非常高,并不能直接运行在计算机硬件机器上。
Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境。
因此,在运行Java程序之前,需要编译器把代码编译成java虚拟机所能识别的指令程序,这就是Java字节码,即class文件。
所以,Java代码运行的第一步是:把Java源代码编译成.class 字节码文件。
类加载
在Class文件中描述的各种信息,需要被加载到虚拟机之后才能运行和使用。因此,需要把class字节码文件加载到Java虚拟机来。
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
加载
在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载阶段完成后,这些二进制字节流按照虚拟机所需的格式存储在方法区之中。
验证
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全,Java虚拟机对输入的字节流走验证过程。
验证阶段包括四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
- 文件格式验证: 验证字节流是否符合Class文件格式规范,如:是否以魔数0xCAFEBABE开头。
- 元数据验证: 对字节码描述的信息进行语义分析,如:这个类的父类是否继承了不允许被继承的类(被final修饰的类);
- 字节码验证: 主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。如:保证跳转指令不会跳转到方法体以外的字节码指令上。
- 符号引用验证: 发生在虚拟机将符号引用转化为直接引用的时候,如:校验符号引用中通过字符串描述的全限定名是否能找到对应的类。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。如:
public static int value =123;
变量value在准备阶段过后的初始值是0而不是123。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
比如:com.User类引用com.Tool类,在编译时,User类不知道Tool类的实际内存地址,因此只能使用符号com.Tool(假设)来表示。而在类加载加载User类的时候,可以通过虚拟机获取Tool类的实际内存地址,因此便可以将符号com.Tool替换为Tool类的实际内存地址,即直接引用地址。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化
到了初始化阶段,才真正开始执行类中定义的Java字节码。在这个阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
创建对象
Java虚拟机是如何执行字节码的呢?我们先来看一下运行时创建对象。
Java是面向对象的编程语言,程序的运行是以对象为调用单位的。
- 字节码文件加载到虚拟机的方法区后,在程序运行过程,通过 class字节码文件创建与其对应的对象信息 。
- 创建对象的方式有:new关键字,反射等。
- Java堆内存是线程共享的区域,创建后的对象信息就保存在Java堆内存中。
方法调用
JVM的调用单位是对象,但是真正执行功能性的代码还是对象上的方法。
在运行过程中,每当调用进入一个java方法,java虚拟机会在当前线程的java方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。方法栈内存是线程私有的,每个线程都有自己的方法栈。如果对应的方法是本地方法,则对应的就是本地方法栈。
java运行时数据区域如下:
解释
当调用Java对象的某个方法时,JVM执行引擎会将该方法的字节码文件翻译成计算机所能识别的机器码,机器码信息保存在方法区中。翻译有解释执行和即时编译两种方式。
两种翻译方式的区别如下:
解释执行来一行代码,解释一行,大部分不常用的代码,都是采用这种方式。
即使编译
对于部分热点代码,将一个方法包含的所有字节码翻译成机器指令,以提高java虚拟机的运行效率。
即时编译是建立经典的二八定律上,即20%代码占据了80%的计算资源。
执行指令
- Java程序被加载入内存后,指令也在内存中了。
- 指令的指令寄存器IP,指向下一条待执行指令的地址。
- CPU的控制单元根据IP寄存器的指向,将主存中的指令装载到指令寄存器,这些加载的指令就是一串二进制码,还需要译码器进行解码。
- 解码后,如果需要获取操作数,则从内存中取数据,调用运算单元进行计算。
多线程上下文切换
CPU一通上电,就会周而复始从内存中获取指令、译码、执行。
- 为了支持多任务,CPU 将执行时间这个资源划分成时间片,每个程序执行一段时间。
- java虚拟机的多线程是通过线程轮流切换分配处理执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条程序中的指令。
- 假设当前线程在运行中,CPU分配的时间执行完了,总得保存运行过的结果信息吧,要不然白白浪费之前的工作了,因此,程序计数器(PC寄存器)作用体现出来了,它是一块较小的内存空间,线程私有,可以看作当前线程执行的字节码的行号指示器。当CPU又给它分配时间跑的时候,可以把数据恢复,接着上一次执行到的位置继续执行就可以了。
原文:https://juejin.im/entry/5e6ccc05e51d4527110aa25f
揭秘Java编程之美掌握这些编码规范,让你的代码一跃成为行业典范
在Java的世界里,编码规范不仅仅是一堆乏味的规则和条款,它们是通往代码优雅之路的黄砖路。想象一下,没有编码规范的Java代码库就像是一场没有裁判的足球赛,混乱不堪,每个人都在按自己的规则踢球。但别担心,今天我们将带你走进编码规范的奇妙世界,探索那些让你的代码从\”看起来还行\”升级为\”哇,这是谁写的神仙代码\”的秘密。
我们将一起探索为什么命名一个变量为temp123可能会让你的同事在code review时掉头就跑,为何空格和缩进在代码中的角色比配角还重要,以及如何通过简单的规则让你的代码变得更加清晰、易读,就像是在阅读一篇畅销小说。
准备好了吗?让我们一起开始我们的编码规范之旅,让你的Java代码不仅仅是运行的艺术,更是视觉的享受。 Buckle up(系好安全带),这将是一次有趣的旅程!
想要成为代码界的建筑大师吗?嗯,让我们从最基础的部分讲起——基础的重要性。你有没有听过那句老话,“只要功夫深,铁杵磨成针”?如果你想让你的代码像针一样锋利、精确,那么你得开始磨练你的基础功夫了。
想象一下,你正在建造一座高楼。如果地基不牢固,那么不管你的建筑有多华丽,最终都会成为倾斜的比萨塔的亲戚。同样的,编程也是这样。如果你的基础不牢固,那么不管你用了多少高级技巧,你的代码最终可能就是一个功能混乱的软件比萨塔。
记住,“千里之行,始于足下”。每一行代码,每一个函数,都是你软件高楼的一砖一瓦。而这些砖瓦的质量,取决于你对基础知识的掌握程度。正如“滴水石穿”,持之以恒的练习和对基础的不断打磨,最终会让你的编程技能坚不可摧。
再来,你听说过“冰冻三尺,非一日之寒”吗?优秀的代码库也是如此,它们的优秀并非一蹴而就,而是基于坚实的基础,经过长时间的积累和迭代。
最后,让我们以“愚公移山”的精神结束这段讨论。面对看似无穷无尽的编程知识,我们可能会感到力不从心。但只要我们坚持不懈,就没有什么山是移不走的,没有什么基础是打不牢的。
所以,亲爱的代码工匠们,让我们从今天开始,把握好每一个学习的机会,把基础打得牢牢的。记住,伟大的软件建筑,都是从一行简单的代码开始的。
1.类名使用UpperCamelCase风格,必须遵从驼峰形式,但以下情形例外:(领 域模型的相关命名)DO / BO / DTO / VO / DAO
深因:
一致性:类名遵循UpperCamelCase(大驼峰式)增加了代码的一致性,使得类名容易识别和区分。
可读性:大驼峰式命名使得多个单词的组合在视觉上更为清晰,有助于理解类的用途。
领域模型例外:DO (Data Object), BO (Business Object), DTO (Data Transfer Object), VO (Value Object), DAO (Data Access Object) 是业界广泛认可的缩写,代表了特定的设计模式和概念。它们的使用在领域驱动设计中具有特定含义,保持这些缩写可以让开发人员快速理解类的职责。
正例(遵循规范):
UserProfile – 明确遵从大驼峰式命名。
UserDTO – 表示一个用于数据传输的对象,DTO作为普遍接受的缩写被保留。
反例(违反规范):
userProfile – 类名以小写字母开头,不符合大驼峰式命名规则。
UserDataObject – 应缩写为UserDO,因为DO是一个被广泛认可的领域模型命名缩写。
通过此规范,可以确保代码的整洁性、一致性和专业性,同时也尊重了行业内的共识和最佳实践。
2.抽象类命名使用Abstract或Base开头
深因:
明确性:以Abstract或Base开头的命名立即明确了该类的抽象性质,让其他开发者一眼就能识别出这是一个不应被直接实例化的类。
可维护性:当项目规模扩大时,清晰的命名规范有助于维持代码的可维护性,减少查找和理解各个类之间关系的时间。
一致性:统一的命名规范有助于保持代码库的一致性,使得新加入的开发人员能更快地熟悉代码库。
避免命名冲突:在有些情况下,抽象类和具体实现类可能会有相似的功能描述,以Abstract或Base开头可以减少命名上的冲突。
正例(遵循规范):
AbstractVehicle:一个定义了交通工具通用属性和方法的抽象类,正确地使用了Abstract前缀。
BaseService:定义了服务层基本功能的抽象类,使用了Base前缀,明确表示这是一个基础类,用于被继承。
反例(违反规范):
Vehicle:假如这是一个抽象类,但没有使用Abstract或Base前缀,这使得它看起来像是一个可直接实例化的类。
Service:如果这是一个旨在被其他服务类继承的基础抽象类,但命名中缺少了表明其抽象性质或基础性质的前缀。
通过引入这条规范,可以提高代码的可读性和维护性,同时减少因命名不当引起的混淆。
3.异常类命名使用Exception结尾
深因:
清晰性:命名中使用Exception结尾能立即明确该类是一个异常类,有助于开发者快速识别其用途和性质。
一致性:这一命名规范与Java标准异常类的命名保持一致,如NullPointerException、IndexOutOfBoundsException等,有助于保持代码的一致性,减少学习成本。
可读性:在阅读代码时,能够通过异常类名直接了解到该异常的大致类型,提高了代码的可读性。
预防错误:明确的命名有助于防止将异常类与普通类混淆,减少因错误处理异常或误用类而引发的bug。
正例(遵循规范):
UserNotFoundException:明确表示寻找用户失败时抛出的异常,正确地使用了Exception结尾。
InvalidInputException:表示输入无效时抛出的异常,使用了Exception结尾,清晰地表明了其是一个异常类。
反例(违反规范):
UserNotFound:虽然意图表达寻找用户失败的情况,但由于没有使用Exception结尾,使得它看起来更像是一个普通类而非异常类。
InvalidInput:这个名字没有明确表明它是一个异常类,可能会被误解为一个方法名或变量名,而不是一个应该被抛出和捕获的异常类型。
通过遵循这条规范,开发者可以更容易地编写和维护清晰、一致且易于理解的异常处理代码。
4.单元测试类命名以它要测试的类的名称开始,以Test结尾
深因:
直观性:当测试类以被测试类的名称开始,紧随其后加上Test作为后缀,这种命名方式直观地表明了测试类的目的和它所测试的具体类,提高了可读性和易理解性。
可查找性:这种命名约定使得开发者可以轻松地通过类名找到对应的测试类,或通过测试类推断出它测试的目标类,从而提高开发效率。
一致性:遵循这一命名规范可以在整个项目或团队中保持一致性,减少因个人偏好导致的命名混乱,使代码库更加整洁。
组织性:在大型项目中,可能会有大量的测试类。这种命名规则有助于在项目结构中保持组织性,使测试包结构清晰、有序。
正例(遵循规范):
假设有一个类名为UserService,那么对应的单元测试类应该命名为UserServiceTest。这明确了UserServiceTest是用于测试UserService类的功能。
对于类PaymentProcessor,其单元测试类应该命名为PaymentProcessorTest,这样一来,仅通过名字就能清楚地知道这是PaymentProcessor类的测试。
反例(违反规范):
如果有一个类名为OrderManager,而其测试类被命名为TestOrderManager或仅仅是OrderTests,这虽然在一定程度上表明了测试目标,但不符合“以被测试类的名称开始,以Test结尾”的规范,可能会导致查找和理解上的不便。
对于InventoryService类,如果其测试类被命名为InventoryChecks,这种命名虽然描述了测试的一般内容,但没有遵循规定的命名模式,降低了命名的一致性和直观性。
遵循这条规范,有助于维护代码的清晰度和组织性,同时也方便团队成员之间的协作和沟通。
5.方法名、参数名、成员/局部变量都统一使用lowerCamelCase,必须遵从驼峰形式
深因:
一致性:在整个项目中统一使用lowerCamelCase(小驼峰命名法)可以保持代码的一致性,使得代码更加整洁和统一。
可读性:lowerCamelCase通过在单词之间使用大小写来区分,无需额外的分隔符,从而提高了代码的可读性和易于理解。
遵循约定:在多数编程语言中,lowerCamelCase是方法名、参数名、成员变量和局部变量的普遍约定,遵循这些约定有助于维持代码风格的一致性,同时也方便其他开发者阅读和理解代码。
减少错误:统一的命名规范有助于减少由于命名不一致导致的混淆和错误。
正例(遵循规范):
方法名:calculateTotalPrice,清晰地表明这是一个计算总价的方法。
参数名:customerName,明确指出传入的是顾客的名字。
成员变量:shoppingCart,代表购物车对象。
局部变量:itemCount,表示物品数量。
反例(违反规范):
方法名:CalculateTotalPrice或calculate_total_price。前者使用了PascalCase(大驼峰命名法),后者使用了snake_case(下划线命名法),都不符合lowerCamelCase的规范。
参数名:CustomerName或customer_name,同样违反了使用lowerCamelCase的规范。
成员变量:ShoppingCart或shopping_cart,没有遵循小驼峰命名法。
局部变量:ItemCount或item_count,同样违反了小驼峰命名法的规定。
通过遵循lowerCamelCase命名规范,可以使代码更加统一和易于理解,促进团队内部和跨团队之间的有效沟通。
6.代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束
深因:
清晰性:避免使用下划线或美元符号作为命名的开始或结束,可以使得代码命名更加清晰,易于阅读和理解。这些符号在很多语言中有特殊含义,过度使用可能导致混淆。
一致性:统一的命名规范有助于保持代码的一致性,减少因个人命名偏好导致的风格差异,使代码库整体更加规范和整洁。
可维护性:清晰和一致的命名规范有助于提高代码的可维护性,便于团队协作和代码的长期维护。
避免冲突:在某些编程语言中,下划线或美元符号用于特殊变量或内部语言机制,避免使用这些符号作为普通命名的一部分,可以减少与语言特性的冲突。
正例(遵循规范):
变量名:userName,明确且易于理解,且没有使用下划线或美元符号作为开头或结尾。
方法名:calculateTotal,遵循了规范,清晰表达了方法的功能。
反例(违反规范):
变量名:_userName或userName_,以及$userName或userName$,这些命名都违反了不使用下划线或美元符号开头或结尾的规范。
方法名:_calculateTotal或calculateTotal_,以及$calculateTotal或calculateTotal$,同样违反了规范。
通过遵守这条规范,可以使代码更加清晰和规范,减少潜在的混淆,促进代码的健康发展和团队间的有效沟通。
7.常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长
深因:
可识别性:全部大写字母加下划线的命名方式使常量在代码中非常容易识别,区分于变量和其他类型的命名。
清晰性:强调语义表达的完整性有助于提高代码的清晰度,即使名称较长,也能确保其意图和用途一目了然。
一致性:遵循此规范能在整个项目或团队中保持命名的一致性,减少因个人偏好导致的风格差异。
避免冲突:通常变量和函数使用小驼峰或大驼峰命名法,常量使用全大写与下划线的方式可以有效避免命名冲突。
正例(遵循规范):
MAX_USER_COUNT:表明了这是一个表示用户数量上限的常量,名称完整表达了其意图。
DEFAULT_PAGE_SIZE:清楚地指出这是默认页面大小的常量,语义明确。
反例(违反规范):
MaxUser或maxUser:虽然意图指代最大用户数,但不符合全大写和单词间用下划线隔开的规范,且看起来更像是变量而非常量。
defaultsize或defaultSize:名称不仅没有全部大写,而且单词间没有使用下划线隔开,语义表达也不够清晰。
通过遵循这条规范,可以显著提高代码中常量的可识别性和清晰性,有助于维护和理解代码。
8.对于Service和DAO类,基于SOA的理念,暴露出来的服务一定是接口,内部的实现类用Impl的后缀与接口区别
深因:
解耦: 通过定义接口,将实现与调用解耦,便于在不同实现间切换,提高了代码的灵活性和可维护性。
易于扩展: 接口定义了一组规范,使得未来扩展或修改功能时,只需添加或修改具体实现类,而不需要修改调用方代码。
便于测试: 接口使得可以使用Mock对象来替代具体实现,便于进行单元测试。
清晰的结构: 接口和实现类的命名规范有助于快速识别类的作用,增加了代码的可读性。
正例(遵循规范):
接口命名为UserService,表明这是一个用户服务的接口。
实现类命名为UserServiceImpl,清楚地表明这是UserService接口的一个具体实现。
反例(违反规范):
接口和实现类命名为UserService和UserServiceImplementation,或者仅仅是在实现类上使用Service作为后缀。这种命名方式不够简洁,且可能导致命名的不一致性。
实现类没有明确使用Impl后缀,例如只是UserManager,这样就不容易区分哪些是接口,哪些是实现类。
通过遵循这条规范,可以提高代码的结构清晰度,便于维护和扩展,同时也符合SOA(面向服务的架构)的设计理念。
9.包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式
深因:
避免平台差异:不同的操作系统对文件名的大小写敏感性不同。统一使用小写可以避免跨平台开发时的混淆和错误。
提高可读性:点分隔符和自然语义的英语单词组合,使得包路径易于理解,反映了项目的结构和内容。
保持一致性:使用单数形式的包名,保持了命名的简洁和一致性,避免了复数形式可能带来的混淆。
灵活性和准确性:允许类名使用复数形式,为表达“集合”或“多个实体”的概念提供了灵活性,使得类的命名更加准确和直观。
正例(遵循规范):
包名:com.example.project.user,使用了小写,点分隔符后是单数形式的自然语义英语单词。
类名:若类表示多个用户,可以命名为Users。
package com.example.project.user;
public class Users {
// 类实现
}
反例(违反规范):
包名:com.Example.Project.Users,使用了大写字母和复数形式,违反了包名全小写和单数形式的规范。
包名:com.example.project.user_info,使用了下划线而不是点分隔符,且包含了多于一个自然语义的英语单词。
package com.Example.Project.Users; // 错误的包命名
public class UserList {
// 类实现
}
通过遵循这条规范,可以使得包结构更加清晰,易于管理,同时也提高了代码的可读性和一致性。
10.POJO类中的任何布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误
深因:
兼容性问题:在Java Bean规范中,布尔类型的属性通常通过is前缀的getter方法来访问。但是,在使用某些序列化框架时,如果字段名本身以is开头,可能会导致框架在生成的getter/setter方法命名上产生混淆,引起序列化或反序列化错误。
清晰的命名约定:避免在变量名中使用is前缀,可以使得命名约定更加清晰。通过getter和setter方法的命名来表达属性的意图,而不是通过变量名本身。
提高代码的可读性和维护性:统一的命名规范有助于提高代码的可读性和维护性,特别是在团队协作中。
正例(遵循规范):
假设有一个布尔类型的变量,表示用户是否已经激活:
public class User {
private boolean active; // 不使用is前缀
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
反例(违反规范):
在变量命名中使用is前缀,可能会与自动生成的getter方法冲突,导致序列化框架解析错误:
public class User {
private boolean isActive; // 使用了is前缀
public boolean isActive() {
return isActive;
}
public void setActive(boolean isActive) {
this.isActive = isActive;
}
}
在这个反例中,某些序列化框架可能期望访问器方法为getIsActive()而不是isActive(),因此,遵循不在布尔类型变量名中使用is前缀的规范,可以避免这类问题。
11.类型与中括号紧挨相连来表示数组
深因:
一致性:将类型和中括号紧挨相连有助于保持代码的一致性,使得数组类型的声明更加清晰和一致。
提高可读性:这种声明方式明确了数组的类型,使得阅读和理解代码变得更加容易。
避免误解:在某些情况下,将中括号放在变量名而不是类型名旁边可能会导致误解,尤其是在声明多个变量时。
正例(遵循规范):
int[] numbers;String[] names;
在这个例子中,类型(int、String)与中括号紧挨相连,清晰地表示了变量是数组类型。
反例(违反规范):
虽然这种声明方式在Java中是合法的,但它不符合“类型与中括号紧挨相连来表示数组”的规范。特别是在声明多个数组或非数组变量时,可能会导致混淆:
int numbers[], size; // size不是数组类型,但这种声明方式可能会导致误解。
通过遵循这条规范,可以提高代码的清晰度和一致性,避免可能的误解,使得代码更加易于阅读和维护。
12.禁止在POJO类中,同时存在对应属性xxx的isXxx()和getXxx()方法
深因:
避免混淆: 同一个属性存在isXxx()和getXxx()方法会造成混淆,不清楚哪个方法应该被使用,尤其是在框架利用反射时。
框架兼容性: 一些框架在处理布尔属性时,对isXxx()和getXxx()方法有特定的预期和处理逻辑。同时存在这两种方法可能导致框架的反射机制工作不正常,进而影响序列化和反序列化行为。
代码清晰性: 保持每个属性只有一个访问器方法可以使得代码更加清晰,易于理解和维护。
Java Bean规范: 根据Java Bean规范,布尔类型的属性应当使用isXxx()形式的访问器方法,而非布尔类型的属性应使用getXxx()方法。
正例(遵循规范):
对于布尔类型的属性active,只提供isActive()方法:
public class User {
private boolean active;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
反例(违反规范):
对于同一个布尔类型的属性active,同时提供了isActive()和getActive()方法:
public class User {
private boolean active;
public boolean getActive() {
return active;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
在这个反例中,同一个属性active提供了两个访问器方法,这可能会导致在使用Java Bean相关框架时出现问题。遵循此规范,确保每个属性只有一种符合其类型的访问器方法,有助于提高代码质量和减少潜在的错误。
1.所有的字段和方法必须要用javadoc注释
深因:
提高代码的可读性和可维护性:通过Javadoc注释,开发者可以快速理解每个字段和方法的用途、参数、返回值等,无需深入阅读实现代码。
促进团队协作:在团队开发中,详细的文档可以帮助新成员快速理解项目,减少沟通成本。
自动生成文档:Javadoc注释可以被工具用来生成标准的HTML格式的API文档,便于分享和查阅。
规范编码风格:强制要求使用Javadoc注释可以规范开发者的编码风格,使得代码整体质量更高。
正例(遵循规范):
/**
* 用户类。
*/
public class User {
/**
* 用户的名字。
*/
private String name;
/**
* 获取用户的名字。
*
* @return 用户的名字
*/
public String getName() {
return name;
}
/**
* 设置用户的名字。
*
* @param name 用户的新名字
*/
public void setName(String name) {
this.name = name;
}
}
反例(违反规范):
public class User {
private String name; // 缺少Javadoc注释
public String getName() { // 缺少Javadoc注释
return name;
}
public void setName(String name) { // 缺少Javadoc注释
this.name = name;
}
}
在反例中,字段和方法都没有Javadoc注释,这使得其他开发者或使用者难以快速理解其用途和功能。
遵循这条规范,可以显著提升代码的可读性和易维护性,同时也有助于自动生成文档。
2.所有的枚举类型字段必须要有注释,说明每个数据项的用途
深因:
明确枚举项的含义:枚举类型通常用来表示一组固定的常量,每个枚举项都有其特定的用途和意义。注释可以帮助开发者快速理解每个枚举项的具体含义,提高代码的可读性。
提高代码的可维护性:随着时间的推移,项目中的枚举类型可能会被多次修改或扩展。有注释的枚举项可以让后来者更容易理解每个枚举项的用途,减少因误解造成的错误。
促进团队协作:在团队协作中,清晰的注释可以减少成员之间的沟通成本,特别是对于新加入的团队成员,有助于他们更快地理解项目代码。
正例(遵循规范):
/**
* 表示用户状态的枚举。
*/
public enum UserStatus {
/**
* 激活状态。用户已激活且可以正常使用系统。
*/
ACTIVE,
/**
* 禁用状态。用户已被禁用,不能登录或使用系统。
*/
DISABLED,
/**
* 等待激活。用户已注册但尚未激活。
*/
PENDING_ACTIVATION
}
反例(违反规范):
public enum UserStatus {
ACTIVE, // 缺少注释
DISABLED, // 缺少注释
PENDING_ACTIVATION // 缺少注释
}
在反例中,UserStatus枚举的每个项都没有注释,这使得其他开发者难以理解每个枚举项的具体含义和用途。
遵循这条规范,通过为每个枚举项添加清晰的注释,可以显著提升代码的可读性和可维护性。
3.方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/**/注释,注意与代码对齐
深因:
提高可读性:通过在被注释语句上方另起一行进行单行注释,可以使得注释更加突出,易于阅读。多行注释同样需要对齐,以保持代码的整洁和一致性。
区分注释类型:使用//进行单行注释和使用/**/进行多行注释可以帮助区分注释的用途和长度,使得代码更加清晰。
维护代码风格一致性:统一的注释风格有助于维护代码的整体风格一致性,无论是在同一项目内还是跨项目。
正例(遵循规范):
public void updateUserInfo() {
// 检查用户是否登录
if (user.isLoggedIn()) {
user.updateLastLoginTime(); // 更新最后登录时间
}
/*
* 多行注释
* 更新用户状态和时间
*/
user.setStatus(\”active\”);
user.setUpdateTime(System.currentTimeMillis());
}
在这个例子中,单行注释使用//并放在被注释语句的上方,而多行注释使用/**/并与代码对齐,保持了代码的清晰和整洁。
反例(违反规范):
public void updateUserInfo() {
user.updateLastLoginTime(); // 检查用户是否登录并更新最后登录时间
/* 更新用户状态
和时间 */
user.setStatus(\”active\”);
user.setUpdateTime(System.currentTimeMillis());
}
在这个反例中,单行注释和被注释的语句在同一行,多行注释没有与代码对齐,这些做法都降低了代码的可读性和整洁性。
遵循这条规范,通过在方法内部恰当地使用单行和多行注释,可以大大提高代码的可读性和维护性。
1.long或者Long初始赋值时,必须使用大写的L,不能是小写的l,小写容易跟数字
深因:
提高代码可读性:小写的l和数字1在很多字体中非常相似,这可能导致阅读代码时的混淆和错误。使用大写的L可以明显区分,提高代码的可读性。
减少错误:在长整型数值的赋值过程中,使用清晰明确的标识可以避免由于误读导致的错误,尤其是在涉及到数值计算的场景中。
统一编码风格:规定在所有场合下使用大写的L为long或Long类型赋值,可以统一代码风格,减少团队内部的差异。
正例(遵循规范):
long count = 1000L;
Long total = 5000L;
在这个例子中,所有的long或Long类型赋值都使用了大写的L,清晰且易于区分。
反例(违反规范):
long count = 1000l;
Long total = 5000l;
在这个反例中,long或Long类型赋值使用了小写的l,这在某些字体中可能与数字1混淆,降低了代码的可读性。
遵循这条规范,通过使用大写的L为long或Long类型赋值,可以有效避免混淆和错误,提高代码的整体可读性。
2.不允许任何魔法值(即未经定义的常量)直接出现在代码中
深因:
提高代码的可读性:使用有意义的常量名代替魔法值可以让代码更易于理解。读者可以通过常量名了解其用途,而不是试图解释一个裸露的数值或字符串的含义。
便于维护:当需要修改一个在多处使用的值时,使用常量可以让你只需要修改定义常量的地方,而不需要逐个修改多处的硬编码值。
减少错误:直接在代码中使用硬编码值,特别是在多处使用时,容易引入错误。例如,如果需要更改该值,可能会遗漏某些实例,导致不一致。
正例(遵循规范):
public class Config {
public static final int MAX_USER_COUNT = 100;
}
// 使用常量
if (userCount > Config.MAX_USER_COUNT) {
// 处理超出用户数限制的情况
}
在这个例子中,100被定义为一个有意义的常量MAX_USER_COUNT,使用这个常量来代替魔法值。
反例(违反规范):
// 直接使用魔法值
if (userCount > 100) {
// 处理超出用户数限制的情况
}
在这个反例中,100直接硬编码在判断语句中,这是一个魔法值的典型使用场景,它降低了代码的可读性和可维护性。
遵循这条规范,通过将魔法值替换为有意义的常量,可以显著提高代码的可读性、可维护性,并减少因硬编码引入的错误。
1. 使用集合转数组的方法,必须使用toArray(T[] array),传入类型完全一样的数组,大小list.size()
深因:
类型安全:使用toArray(T[] array)方法并传入类型完全一样的数组可以保证转换结果的类型安全。这样可以避免在运行时因类型不匹配而抛出异常。
性能优化:如果传入的数组大小与集合大小一致(list.size()),则该数组将被直接使用,避免了额外的数组分配和复制,提高了效率。
避免使用反射:与toArray()方法相比,toArray(T[] array)避免了在内部使用反射来创建返回数组,因此可以提高性能。
正例(遵循规范):
List<String> list = new ArrayList<>();
list.add(\”apple\”);
list.add(\”banana\”);
// 使用toArray(T[] array)传入类型完全一样的数组,大小为list.size()
String[] array = list.toArray(new String[list.size()]);
在这个例子中,传入了一个类型和大小都符合要求的数组给toArray方法,这是符合规范的做法。
反例(违反规范):
List<String> list = new ArrayList<>();
list.add(\”apple\”);
list.add(\”banana\”);
// 使用无参toArray()方法
Object[] array = list.toArray();
// 或使用大小不符合list.size()的数组
String[] smallerArray = list.toArray(new String[0]);
在这两个反例中,第一个示例使用了无参的toArray()方法,返回了一个Object[]类型的数组,这不是类型安全的。第二个示例虽然使用了toArray(T[] array)方法,但传入的数组大小不是list.size(),这可能导致额外的数组分配和复制,降低了效率。
遵循这条规范,通过使用toArray(T[] array)方法并传入一个大小为list.size()的类型匹配数组,可以在类型安全的同时提高性能。
2.使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常
深因:
避免运行时异常:Arrays.asList()返回的是一个固定大小的列表,它基于原数组,仅支持那些不会改变数组大小的操作。尝试执行add、remove或clear等修改操作会抛出UnsupportedOperationException。避免使用这些操作可以防止运行时异常。
提醒开发者注意返回类型限制:明确这一规范可以提醒开发者,通过Arrays.asList()获得的列表在功能上与ArrayList等完全实现了List接口的集合类有所不同,需要谨慎处理。
促进正确的集合操作使用:引导开发者在需要进行元素增删的场景中,选择更合适的集合类型,如直接使用ArrayList等,或者在需要对Arrays.asList()返回的列表进行修改操作时,先将其转换为一个支持所有List操作的新列表。
正例(遵循规范):
String[] array = {\”apple\”, \”banana\”, \”cherry\”};
List<String> list = new ArrayList<>(Arrays.asList(array));
// 修改集合
list.add(\”date\”);
list.remove(\”banana\”);
在这个例子中,通过将Arrays.asList()的结果放入一个新的ArrayList中,我们可以自由地对返回的集合进行修改。
反例(违反规范):
String[] array = {\”apple\”, \”banana\”, \”cherry\”};
List<String> list = Arrays.asList(array);
// 尝试修改集合
list.add(\”date\”); // 抛出UnsupportedOperationException
list.remove(\”banana\”); // 抛出UnsupportedOperationException
在这个反例中,直接使用Arrays.asList()返回的列表进行修改操作,将会导致UnsupportedOperationException异常。
遵循这条规范,可以避免在运行时因尝试修改不支持修改操作的集合而产生异常,确保代码的健壮性。
3.ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常
深因:
类型不兼容:ArrayList的subList方法返回的是java.util.List接口的一个视图,而不是ArrayList实例。这个视图是内部类(如ArrayList$SubList),并不是ArrayList类本身。因此,尝试将其强制转换为ArrayList会因类型不匹配而抛出ClassCastException。
保持视图的动态性:subList返回的列表视图提供了对原列表的部分视图,对这个视图的所有操作都会反映在原列表上(反之亦然)。这种设计意味着视图与原列表紧密相连,而直接转换为ArrayList不仅不可能,也会误导开发者忽视subList与原列表的动态关系。
促进正确的API使用:明确这一规范可以鼓励开发者使用正确的类型和方法来处理集合和子集合,避免不必要的类型转换错误,提高代码质量和可维护性。
正例(遵循规范):
ArrayList<String> list = new ArrayList<>(Arrays.asList(\”apple\”, \”banana\”, \”cherry\”, \”date\”));
List<String> subList = list.subList(1, 3);
// 正确使用subList结果
System.out.println(subList);
在这个例子中,subList的结果被正确地处理为List类型,没有进行不当的类型转换。
反例(违反规范):
ArrayList<String> list = new ArrayList<>(Arrays.asList(\”apple\”, \”banana\”, \”cherry\”, \”date\”));
// 尝试将subList结果强转为ArrayList
ArrayList<String> subList = (ArrayList<String>)list.subList(1, 3); // 抛出ClassCastException
在这个反例中,尝试将subList的结果强制转换为ArrayList类型,这将导致ClassCastException异常。
遵循这条规范,可以避免不必要的类型转换异常,更加准确地使用Java集合框架提供的API,提高代码的健壮性和可读性。
4.在subList场景中,高度注意对原列表的修改,会导致子列表的遍历、增加、删除均产生ConcurrentModificationException异常
深因:
维护一致性:subList方法返回的子列表是原列表的一个视图,这意味着对原列表或子列表的任何修改都会反映在另一方。如果在遍历子列表的同时修改原列表,将破坏列表的结构,因此为了维护操作的一致性和预期行为,Java会抛出ConcurrentModificationException。
防止不可预见的行为:在对原列表进行修改后继续操作子列表可能会导致不可预见的行为,因为子列表的内容、大小和预期操作结果可能已经由于原列表的修改而发生变化。
提升代码稳定性和可靠性:通过避免在子列表操作过程中修改原列表,可以防止运行时异常,从而提高代码的稳定性和可靠性。
正例(遵循规范):
ArrayList<String> list = new ArrayList<>(Arrays.asList(\”apple\”, \”banana\”, \”cherry\”, \”date\”));
List<String> subList = list.subList(1, 3);
// 在操作子列表之前不修改原列表
subList.remove(\”banana\”); // 安全操作
System.out.println(list); // 输出修改后的原列表和子列表
在这个例子中,在对子列表操作之前没有对原列表进行修改,遵循了规范。
反例(违反规范):
ArrayList<String> list = new ArrayList<>(Arrays.asList(\”apple\”, \”banana\”, \”cherry\”, \”date\”));
List<String> subList = list.subList(1, 3);
// 在遍历子列表时修改原列表
list.add(\”fig\”); // 修改原列表
subList.get(0); // 尝试访问子列表,可能抛出ConcurrentModificationException
在这个反例中,修改原列表后尝试访问子列表,这种操作违反了规范,因为它可能导致ConcurrentModificationException异常。
遵循这条规范,可以避免因为列表的并发修改而导致的异常,保证代码的稳定性和预期行为。
5.不要在foreach循环里进行元素的remove/add操作,remove元素请使用Iterator方式
深因:
避免ConcurrentModificationException异常:在foreach循环中对集合进行修改(如添加或删除元素)会导致快速失败(fail-fast)机制触发,抛出ConcurrentModificationException。这是因为foreach循环基于集合的Iterator,而直接修改集合会导致迭代器的状态与集合的状态不一致。
保持代码的稳定性和可预测性:使用迭代器的remove方法可以安全地在遍历过程中删除元素,因为它会正确地更新迭代器的状态,避免异常发生,从而保持代码的稳定性和可预测性。
提高代码可读性和维护性:遵循此规范有助于提高代码的可读性和维护性,因为使用迭代器进行元素的删除是一种更明确和安全的方式,其他开发者能够更容易理解代码的意图。
正例(遵循规范):
List<String> list = new ArrayList<>(Arrays.asList(\”apple\”, \”banana\”, \”cherry\”));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if (\”banana\”.equals(fruit)) {
iterator.remove(); // 使用Iterator的remove方法安全删除元素
}
}
System.out.println(list); // 输出: [apple, cherry]
在这个例子中,通过使用Iterator的remove方法安全地在遍历过程中删除元素,遵循了规范。
反例(违反规范):
List<String> list = new ArrayList<>(Arrays.asList(\”apple\”, \”banana\”, \”cherry\”));
for (String fruit : list) {
if (\”banana\”.equals(fruit)) {
list.remove(fruit);
}
}
在这个反例中,尝试在foreach循环中直接从集合中删除元素,这违反了规范,因为这样做可能会导致ConcurrentModificationException异常。
遵循这条规范,可以避免在遍历集合时因修改集合而导致的异常,保证代码执行的稳定性和安全性。
6.集合初始化时,指定集合初始值大小
深因:
提高性能:指定集合初始值大小可以减少在添加元素时动态扩容的次数,从而减少内存分配和复制旧数组到新数组的开销,特别是在我们预先知道将要存储多少元素时。
减少内存浪费:通过精确或接近精确指定初始容量,可以避免分配比实际需要更多的内存空间,从而减少内存浪费。
提升代码的可读性和意图明确性:在初始化时指定集合大小,可以让后来的代码维护者更清楚地了解开发者的意图,即对集合大小的预期。
正例(遵循规范):
// 假设我们已知需要存储5个元素
List<String> list = new ArrayList<>(5);
在这个例子中,我们预先知道列表将存储5个元素,因此在初始化时指定了初始容量为5。这样可以减少动态扩容的次数。
反例(违反规范):
// 未指定初始值大小
List<String> list = new ArrayList<>();
// 假设后续代码中添加了大量元素
for (int i = 0; i < 1000; i++) {
list.add(\”element\” + i);
}
在这个反例中,初始化时没有指定集合的初始大小。如果后续代码需要添加大量元素,这将导致多次动态数组扩容,从而影响性能。
遵循这条规范,可以帮助提高集合操作的性能,减少内存浪费,同时使代码更加清晰和高效。
不行了,不行了太水了,我装不下去了哈哈,搞点容易被忽略但又确实很重要的规范来讲吧
1.禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象
做过对数字敏感业务的大佬们应该对这个不陌生吧,尤其是做过财务的大佬,想必体验会更深刻,计算值不对,看代码没问题,但是最终结果就是不对,直到你发现精度丢失
深因:
精度问题:直接使用 BigDecimal(double) 构造方法可能会导致精度不准确。double 类型的值在转换为 BigDecimal 对象时,可能无法精确表示原始 double 值,因为 double 是一个浮点类型,其表示方式和精度与 BigDecimal 的不同。
可预测性:使用 String 参数或 BigDecimal.valueOf(double) 方法创建 BigDecimal 对象可以避免因浮点表示导致的预料之外的精度问题,使结果更加可预测。
避免隐藏的bug:不精确的转换可能在数值处理中引入难以发现的错误,特别是在财务计算或需要高精度的场景中,这些错误可能导致严重后果。
正例(遵循规范):
BigDecimal correct = new BigDecimal(\”0.1\”);
// 或者
BigDecimal alsoCorrect = BigDecimal.valueOf(0.1);
这两种方式都能准确地表示 0.1,而不会引入由 double 类型的精度问题导致的误差。
反例(违反规范):
BigDecimal problematic = new BigDecimal(0.1);
这种方式使用 double 构造方法创建 BigDecimal 实例,可能无法精确表示 0.1,因为 0.1 在 double 类型中是一个近似值。
遵循这条规范,可以确保使用 BigDecimal 时的精度和可预测性,避免在财务计算和需要高度精确的应用中引入隐蔽的错误。
2.避免采用取反逻辑运算符,\’!\’运算符不利于快速理解
这个规则看起来没什么特别深奥的,但是确实是容易被大家忽略的,在下就曾经踩过此雷,注释和逻辑中的!用的都是非常规的反向注释和逻辑,结果导致理解起来容易出错,甚至在下看见过一个if里边包了很多个判断条件,各种小括号,而且都是用的反向!逻辑,搞得一点也看不懂,真是耗时又耗力,而且极其容易出错
深因:
提高代码可读性:避免使用取反逻辑运算符 \’!\’ 可以使代码逻辑更直观,更易于理解。对于一些人来说,直接处理肯定的情况比处理否定的情况更为直接和易懂。
减少理解错误:在复杂的逻辑表达式中,使用 \’!\’ 可能会增加理解和解析表达式所需的认知负担,尤其是在多重否定(如 !!)或者是在多个逻辑运算混合使用时。
避免逻辑错误:简化逻辑表达式有助于减少逻辑错误的发生,特别是在进行条件判断时,直接的条件判断比间接的取反判断更不容易出错。
正例(遵循规范):
if (isAvailable) {
// 执行操作
}
在这个例子中,直接检查条件是否满足,而不是它的否定形式,这使得逻辑更直接、更清晰。
反例(违反规范):
if (!isUnavailable) {
// 执行操作
}
这个例子使用了取反逻辑运算符 \’!\’ 来检查条件,这要求阅读代码的人需要进行双重否定的逻辑推理,增加了理解代码的难度。
遵循这条规范有助于提高代码的可读性和直观性,减少因逻辑理解错误而引入的bug,特别是在条件判断复杂或多重逻辑运算时尤为重要。
3.Mybatis自带的queryForList(String statementName,int start,int size)不推荐使用
深因:
性能问题:queryForList(String statementName, int start, int size) 方法在处理分页时,会先查询出所有符合条件的记录,然后在返回结果列表中根据 start 和 size 参数返回子列表。这意味着,如果数据量很大,即使只需要很少的数据,也会先加载全部数据到内存中,这将导致严重的性能问题。
资源浪费:由于该方法首先加载所有数据到内存,对于数据库和应用服务器来说,这无疑增加了额外的负担,可能导致内存溢出或响应时间变长,影响用户体验。
现代替代方案:随着MyBatis版本的更新,更推荐使用分页插件来进行分页查询。这些方法更加高效,因为它们能够直接在数据库层面上限制查询的范围,避免不必要的数据加载和处理。
正例(遵循规范):
使用插件进行分页查询:
PageHelper.startPage(pageNum, pageSize);
List<Object> list = sqlSession.selectList(\”statementName\”);
反例(违反规范):
int start = 0; // 开始的记录索引
int size = 10; // 需要获取的记录数量
List<Object> list = sqlSession.queryForList(\”statementName\”, start, size);
在这个反例中,使用了不推荐的 queryForList(String statementName, int start, int size) 方法进行分页查询,可能会导致性能问题和资源浪费。
遵循这条规范,可以提高应用的性能和资源使用效率,同时也是向现代化MyBatis使用方式迈进的一步。
4.线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的人员更加明确线程池的运行规则,规避资源耗尽的风险
深因:
明确线程池参数:通过直接使用 ThreadPoolExecutor 构造方法创建线程池,可以让开发者明确指定线程池的核心参数,如核心线程数、最大线程数、存活时间、工作队列等,这有助于开发者深入理解线程池的工作原理和性能特性。
避免资源耗尽:使用 Executors 类的静态方法创建线程池(如 Executors.newCachedThreadPool() 和 Executors.newFixedThreadPool())时,可能会由于不恰当的配置导致资源耗尽。例如,newCachedThreadPool 默认允许创建的线程数量几乎是无限的,这可能会导致大量线程同时运行,从而耗尽系统资源。
增强可维护性:明确线程池的配置参数,有助于后期维护和调优,因为这些参数直接影响到线程池的性能和系统资源的使用。
正例(遵循规范):
int corePoolSize = 10;
int maximumPoolSize = 100;
long keepAliveTime = 1L;
TimeUnit unit = TimeUnit.MINUTES;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue
);
在这个例子中,通过 ThreadPoolExecutor 直接构造线程池,所有重要的参数都被明确指定,更加透明和可控。
反例(违反规范):
ExecutorService executor = Executors.newFixedThreadPool(100);
// 或者
ExecutorService executor = Executors.newCachedThreadPool();
这两个反例虽然可以快速方便地创建线程池,但隐藏了线程池的具体实现细节和参数配置,可能会因为不合理的默认配置导致性能问题或资源耗尽。
遵循这条规范,可以提升线程池的使用效率和安全性,减少因线程池不当使用导致的系统资源问题。
5.多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题
深因:
增强的健壮性:使用 ScheduledExecutorService 相比于 Timer,其能够确保即使某个定时任务因异常终止,其他任务仍然可以继续运行。这是因为 ScheduledExecutorService 内部对任务的调度是相互独立的。
更灵活的错误处理:ScheduledExecutorService 允许开发者对每个任务的执行进行更细粒度的控制,包括异常处理。这样,开发者可以针对特定的异常情况实施相应的处理策略,而不是让一个未捕获的异常影响到整个定时任务的执行。
更丰富的功能:ScheduledExecutorService 提供了比 Timer 更为丰富和灵活的调度功能,包括但不限于支持多线程并行执行任务、支持任务的周期性执行以及延迟执行等。
正例(遵循规范):
在这个例子中,使用 ScheduledExecutorService 创建了一个包含5个线程的线程池,并安排了两个任务定期执行。每个任务的执行是独立的,一个任务的失败不会影响到另一个。
反例(违反规范):
在这个反例中,使用 Timer 安排了两个定时任务。如果任务2抛出了未捕获的异常,将会导致整个 Timer 的执行线程终止,从而导致任务1也会停止执行。
遵循这条规范,可以提高多线程并行处理定时任务的健壮性和可靠性,避免单个任务失败导致整个定时任务系统的崩溃。
6.必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题
深因:
防止内存泄露:ThreadLocal 变量存储在每个 Thread 的一个 ThreadLocalMap 中,如果不手动清理,即使外部引用被回收,ThreadLocal 变量仍然可能长时间存活在 Thread 中,导致内存泄露,尤其是在使用线程池时,线程是被复用的,这种情况更为严重。
保证数据隔离性:在线程池场景下,线程被复用,如果不清理 ThreadLocal,可能会导致一些敏感数据被后续执行的任务访问到,这违反了数据隔离的原则,可能影响业务逻辑的正确性。
提高系统稳定性:及时清理 ThreadLocal 变量,可以避免不必要的资源占用和潜在的内存泄露问题,从而提高系统的稳定性和性能。
正例(遵循规范):
public class ExampleThreadLocal {
private static final ThreadLocal<Object> myThreadLocal = new ThreadLocal<>();
public void doSomething() {
try {
myThreadLocal.set(new Object()); // 使用 ThreadLocal
// 业务逻辑
} finally {
myThreadLocal.remove(); // 在 finally 块中清理 ThreadLocal
}
}
}
在这个例子中,myThreadLocal 在使用完毕后,通过 finally 块确保了其被清理,这样即使在使用线程池的情况下,也不会有内存泄露或数据污染的风险。
反例(违反规范):
public class ExampleThreadLocal {
private static final ThreadLocal<Object> myThreadLocal = new ThreadLocal<>();
public void doSomething() {
myThreadLocal.set(new Object()); // 使用 ThreadLocal 但没有清理
// 业务逻辑,缺少清理操作
}
}
在这个反例中,myThreadLocal 被设置了值,但在方法结束时没有被清理。这在单次使用 Thread 的场景中可能看起来没什么问题,但如果在线程池场景下,这个 Thread 可能被重复利用,会导致内存泄露和数据污染。
遵循这条规范,可以有效避免使用 ThreadLocal 变量时的内存泄露问题,并保持数据的隔离性,提升系统的稳定性和安全性。
在遥远的Java王国中,住着一群热爱代码的居民。他们日以继夜地编写代码,希望能创造出令人惊叹的软件奇迹。然而,并非所有代码都能成为传说中的英雄。为什么呢?因为,在这片繁荣的土地上,有一个被忽视的古老法典——Java基本编码规范。
有人可能会问:“为什么我们需要编码规范?难道让代码能跑就不够好吗?”哦,亲爱的朋友,这就好比问为什么超级英雄要穿紧身衣一样。答案很简单——为了让一切看起来更加整洁、有序,以及……好吧,主要是为了看起来酷炫。
编码规范的重要性不仅仅在于它让代码看起来像是由单一神秘编程大师在一夜之间完成的,而且它还帮助我们避免了许多潜在的灾难。比如,一个没有遵循规范的代码库,就像是一个没有交通规则的城市,处处是事故现场,每个人都自行其是,结果只能是混乱一片。
回想下我们之前由于编码问题几个教训:有生产事故的,有遇到问题难以排查的等等。
命名规范:想象一下,如果你的同事把所有的变量都命名为 a1,a2,b1……这不是在写代码,这简直是在玩一场“猜猜我是谁”的游戏!
缩进和格式化:没有一致的缩进和格式化,阅读代码就像是在解读古埃及象形文字。每个人都觉得自己是对的,但最后只能靠猜。
避免使用ThreadLocal未清理:这就像是你的室友用过浴室后不打扫——一次两次还好,时间长了,你会发现自己生活在一个生物危机现场。
因此,亲爱的Java居民们,让我们一起遵守这些神圣的编码准则吧。就像穿上了超级英雄的紧身衣,让我们的代码更加健壮、优雅,并且……当然,更加酷炫!
记住,好的编码规范不仅能让你的代码“活”得更久,还能让后来者在阅读你的代码时,不至于想要穿越时空来找你算账。最后,让我们共勉之——在代码的世界里,每一个规范的遵守,都是向着成为编程界超级英雄迈出的一步。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。