华为大佬万字长文总结,梳理的Java入门所有基础知识点,快收藏
今日分享开始啦,请大家多多指教~
1.1 Java 语言概述。
Java 是一种高级编程语言,而且是面向对象的编程语言。Java 语言是美国 Sun 公司(Stanford University Network),在 1995 年推出的高级的编程语言。Java 语言共同创始人之一:詹姆斯·高斯林 (James Gosling),被称为 Java之父。Java 语言的版本:1.0-1.4,5.0…8.0…13.0,本文笔记用的是 jdk 版本为 8.0。
1.2 Java 语言能做什么
Java 语言主要应用在互联网程序的开发领域,网上购物商城、物流、金融、各行各业的门户网站。
1.3 Java 语言的跨平台实现原理
(1) JVM: Java 虚拟机,是专门用来运行 Java 程序的。
(2) 平台:指的就是操作系统,比如 Windows、linux、MacOS 等。
(3) 跨平台: 我们编写的一个 Java 程序,可以做多个操作系统上运行一次编译,到处运行。
(4) 问题思考,如下:
- Java 程序是跨平台的? ⇒ 正确的 ⇒ 一次编译到处运行。
- JVM 是跨平台的? ⇒ 错误的 ==> JVM 是实现 Java 程序跨平台的基石。针对不同的操作系统提供不同的 JVM。而程序在 JVM 中运行。
- Java 程序的跨平台是依靠 JVM 的不跨平台实现的。正确的
1.4 JDK、JRE、JVM 的组成和作用
- JVM: Java 虚拟机,是专门用来运行 Java 程序的,但是不能单独安装。
- JRE: Java 运行环境,包含 JVM(Java 虚拟机,是专门用来运行 Java 程序的)和核心类库。
- JDK: Java 开发工具包,包含 JRE 和开发工具。
- 三者关系: JDK > JRE > JVM
1.1 Java 语言开发环境搭建
JDK 安装,注意事项:
- 注意操作系统是 Windows、linux、MacOS
- 注意操作系统的位数是 32 位还是 64 位
- 安装 java 相关软件的时候: 安装路径中不允许出现中文和空格(任何开发软件都最好不要安装在中文路径下)
1.2常用 DOS 命令的使用
如何进入DOS 命令操作窗口?
1.开始/命令提示符
2.开始/搜索程序和文件 输入 cmd
3.Windows键 + R –> 输入 cmd
4.窗口空白处/按住 shift 键 + 鼠标右键单击 /在此处开命令窗口
5.常用命令如下表所示:
1.3环境变量 JAVA_HOME 的配置
记事本软件的启动方式?
1.开始/程序/附件/记事本
2.C:/windows/找到notepad.exe命令,双击启动
3.如果在DOS窗口的命令中:
C:\\windows> notepad.exe回车 运行这个命令
首先在C:\\windows路径下,寻找是否存在notepad.exe,发现有,直接运行
D:\\abc> notepad.exe回车 运行这个命令
首先:在 D:\\abc 路径下,寻找是否存在 notepad.exe,发现没有
其次: 如果发现在当前路径 D:\\abc 没有要运行的 notepad.exe 命令,到系统环境变量 path 中寻找
path:… C:\\Windows;…,发现path中配置的路径 C:\\Windows 有该命令,直接运行.
如果path中配置的所有的路径中都没有要运行的命令,运行报错了.
给Java配置环境变量的意义/目的/作用?
让我们可以在任意路径下运行java开发的相关工具(javac: 编译工具,java: 运行工具)
比如jdk的安装路径:C:\\develop\\Java\\jdk1.8.0_162
配置步骤:
1.创建名称为 JAVA_HOME 的环境变量,取值是 C:\\develop\\Java\\jdk1.8.0_162
2.把步骤1中创建的名称为 JAVA_HOME 的环境变量,添加到系统环境变量 path 中
找到系统环境变量path, 在前面添加: %JAVA_HOME%\\bin;…
3.如果在DOS窗口的命令中:
C:\\develop\\Java\\jdk1.8.0_162\\bin> javac.exe回车 运行这个命令
首先在C:\\develop\\Java\\jdk1.8.0_162\\bin路径下,寻找是否存在javac.exe,发现有,
直接运行
D:\\abc> javac.exe回车 运行这个命令 首先:在D:\\abc路径下,寻找是否存在javac.exe,发现没有
其次: 如果发现在当前路径 D:\\abc 没有要运行的 javac.exe 命令,到系统环境变量path中寻找
path:… %JAVA_HOME%\\bin;…, 发现 path 中配置的名称为 JAVA_HOME 的环境变量,对应的路径
C:\\develop\\Java\\jdk1.8.0_162\\bin 中有要运行的 javac.exe 命令,直接运行,
如果 path 中配置的所有路径中,都没有要运行的 javac.exe 命令,运行报错了
寻找名称为JAVA_HOME的环境变量,找到后,使用其配置的具体路径进行替换:
path:… C:\\develop\\Java\\jdk1.8.0_162\\bin;…,
替换后的路径: C:\\develop\\Java\\jdk1.8.0_162\\bin 中有 javac 命令,就可以直接运行
2.1 程序开发的步骤
1. 源程序:
- 程序员写的程序;
- 程序员在自己可以看得懂得程序;
- 程序:字母、数字、其他符号;
源程序是程序员编写的,程序员自己可以看得懂得程序,本质就是一个文本文件,但是扩展名不是 .txt,而是 .java。
2. 生产JVM可以执行的字节码(.class)文件
- JVM:叫做 Java 虚拟机,是专门用来运行 Java 程序的。但是 JVM 是一个二货,只能识别 0 和 1,而存储 0 和 1 的文件叫做 字节码文件(.class文件)
- 如何把源文件(程序)翻译成JVM能够执行的字节码文件(程序)呢? 使用 javac 命令(编译命令), 使用格式:javac 文件名.java 例如:编译HelloWorld.java 源文件: javac HelloWorld.java,生成一个字节码文件:HelloWorld.class
3. 把字节码文件交给JVM执行
不管是源文件(程序)还是字节码文件(程序)都存储在硬盘中? 不会自动执行,如何把字节码文件交给 JVM 执行呢? 使用 java 命令(运行命令)。 使用格式: java 文件名 例子:java HelloWorld。
2.2 HelloWorld 案例的编写编译运行
1. 编写源文件。 创建一个名称为 HelloWorld.txt 的文本文件,把扩展名修改为 .java,打开 HelloWorld.java 源文件,输入以下内容,并保存(ctrl+s)。
2. 编译: javac命令。 根据 .java源文件 生产对应的 .class文件(字节码文件)。 使用 javac 命令的格式:javac 文件名.java。javac HelloWorld.java。注意:
- 保证当前路径下 javac命令 可以使用。
- 保证当前路径下有要进行编译的 源(.java)文件。
- 使用编译javac命令时,文件名后面必须写 扩展名.java。
3.运行: java命令。 把 字节码(.class)文件 交给 jvm 执行。使用 java 命令的格式:java 文件名,java HelloWorld,同样要注意:
- 保证当前路径下 java 命令 可以使用。
- 保证当前路径下有要进行运行的 字节码(.class)文件。
- 使用运行 java 命令 时,文件名后面不能写 扩展名.class。
2.3 初学者编写 HelloWorld 常见问题
- 非法字符问题。Java 中的符号都是英文格式的。
- 大小写问题。Java 语言对 大小写敏感(区分大小写)。
- 在系统中显示文件的扩展名,避免出现 HelloWorld.java.txt 文件。
- 编译命令后的 java文件名需要带文件后缀 .java。
- 运行命令后的 class文件名(类名) 不带文件 后缀.class。
- 不要把 main 写成 mian。
2.4 Notepad++ 软件的安装和配置
3.1 注释
概念: 在代码中添加注释可提高代码的可读性。注释中包含了程序的信息,可以帮助程序员更好地阅读和理解程序。在 Java 源程序文件 的任意位置都可以添加注释,且 Java 编译器不编译代码中的注释,也就是说代码中的注释对程序不产生任何影响。所以开发者不仅可以在注释中编写代码的说明文字、设计者的个人信息,还可以使用注释来屏蔽某些不希望执行的代码。
分类: Java 提供了 3 种代码注释,分别为单行注释、多行注释和文档注释。
1、单行注释:// 为单行注释标记,从符号 // 开始直到换行为止的所有内容均作为注释而被编译器忽略。语法格式如下:
// 注释内容
int age ; // 声明int型变量,用于保存年龄信息
2、多行注释: /* */ 为多行注释标记,符号 /* 与 */ 之间的所有内容均为注释内容。注释中的内容可以换行。语法格式如下:
3、文档注释: /**…*/ 为文档注释标记。符号 /** 与 */ 之间的内容均为文档注释内容。当文档注释出现在声明(如类的声明、类的成员变量声明、类的成员方法声明等)之前时,会被 Javadoc 文档工具 读取作为 Javadoc 文档内容。文档注释的格式与多行注释的格式相同。对于初学者而言,文档注释并不是很重要,了解即可。示例如下:
说明:一定要养成良好的编码习惯。软件编码规范中提到 可读性第一,效率第二,所以程序员必须要在程序中添加适量的注释来提高程序的可读性和可维护性。建议程序中的注释总量要占程序代码总量的 20%~50%。
3.2 关键字
引入:邮箱: @前面是用户名,@后面是使用的是哪家的邮箱。
换而言之,关键字是 Java 中已经被赋予特定意义的一些单词,不可以把这些字作为标识符来使用。关键字中的所有字母都是小写的,或者在高级编辑器中彩色显示。
Java中的关键字如下表所示:
3.3 标识符
标识符可以简单地理解为一个名字,用来标识类名、变量名、方法名、数组名等的有效字符序列。Java 规定标识符由 任意顺序的字母、下划线(_)、美元符号($)和数字组成,并且第一个字符不能是数字。标识符不能是 Java 中的保留关键字。 示例:
常见错误:用中文命名标识符是非常不好的编码习惯。当编译环境的字符编码集发生改变后,代码中所有的中文标识符全部会显示成乱码,程序将无法维护。因为 Java 是一种可以跨平台的开发语言,所以发生中文标识符显示成乱码这种情况的概率非常大。编写 Java 代码有一套公认的命名规范:
- 类名:通常使用名词,第一个单词字母必须大写,后续单词首字母大写。(大驼峰式)
- 方法名:通常使用动词,第一个单词首字母小写,后续单词首字母大写。(小驼峰式)
- 变量:第一个单词首字母小写,后续单词首字母大写。(小驼峰式)
- 常量:所有字母均大写。
- 单词的拼接:通常使用 userLastName 方式拼接单词,而不是 user_last_name。
4.1 常量的概念和分类
引入:
数学中的常数,对应到 java 中叫常量,数学中的常数有分类,java 中的常量也有分类,而且比数学中的分类更加丰富。
1、概念: 在程序的执行过程中,其值不可以发生改变的量。
2、分类:
- 整数常量:1314、520
- 小数常量:13.14、5.20
- 字符常量:java 中规定字符常量必须使用单引号 \’\’ 引起来,而且单引号 \’\’ 中只能写一个字符(不能不写,也不能写2个以上) 举例:
- 布尔常量:只有两个值 true 和 false。true:表示肯定的,对的,是的,正确的,成立的。false:表示否定的,错的,不是的,却无的,不成立的。
- 字符串常量:java 中规定字符串常量必须使用双引号 \”\” 引起来,而且双引号 \”\” 中可以写多个字符(0个、1个、2个…), 举例:
- 空常量:null
4.2 打印不同类型的常量
同 C语言 一样,Java 程序想要在控制台输出文字,需要调用一个已有的方法,方法如下:
System.out.print(\”Hello!\”); // 此方法输出\”Hello\”后不会自动换行,光标停留同一行的末尾
但与 C语言 不同的是,Java 又提供了一个输出文字后自动换行的方法,这个方法在原有的 print 后面加上了 ln 后缀,方法如下:
System.out.println(\”Hello!\”); // 此方法输出\”Hello\”后会自动换行,光标停留下一行的开头
打印不同类型的常量,示例如下:
4.3 变量和数据类型【重要】
引入:
1、变量概念: 在程序的执行过程中,其值可以在一定范围内发生改变的量。可以把变量理解成为一个 容器,例如一个空烧杯,给变量赋值就相当于给烧杯倒水。如下图所示的那样,变量可以不断更换值,就像烧杯可以反复使用一样:
2、分类:
3、变量定义格式图解分析:
变量的理解:
- 变量的本质就是内存中的一块空间,空间的大小由数据类型决定。
- 要想找到变量对应的内存空间的数据,需要给变量对应的内存空间起个名字,叫做变量名称。对于变量的命名并不是任意的。
应遵循以下几条规则:
a.变量名必须是一个有效的标识符。
b.变量名不可以使用 Java 中的关键字。
c.在同一个大括号范围内,变量名不能重复。
d.应选择有意义的单词作为变量名。
说明:在 Java 中允许使用汉字或其他语言文字作为变量名,如 **int 年龄 = 21;** 在程序运行时不会出现错误,但建议尽量不要使用
这些语言文字作为变量名。
- 变量对应的内存空间中必须有数据才能使用,这种向变量内存空间中,存储数据的过程叫做初始化或者赋值。
图解:
为什么要声明变量呢?简单地说,就是要告诉编译器这个变量属于哪一种数据类型,这样编译器才知道需要分配多少空间给它,以及它可以存放什么样的数据。
4、定义8种变量1:
5、定义8种变量2:
变量的注意事项:定义的变量,不赋值不能使用。定义 long 类型的变量时,需要在整数的后面加 L(大小写均可,建议大写)。同理,定义 float 类型的变量时,需要在小数的后面加 F(大小写均可,建议大写)。
4.4 数据类型转换
类型转换是将一个值从一种数据类型更改为另一种数据类型的过程。例如,可以将 String 类型数据 457 转换为一个数值型,而且可以将任意类型的数据转换为 String 类型。数据类型转换有两种方式,即 隐式转换与显式转换。 如果从低精度数据类型向高精度数据类型转换,则永远不会溢出,并且总是成功的;
而把高精度数据类型向低精度数据类型转换则必然会有信息丢失,甚至有可能失败。这种转换规则就像下图所示的两个场景,高精度相当于一个大水杯,低精度相当于一个小水杯,大水杯可以轻松装下小水杯中所有的水,但小水杯无法装下大水杯中所有的水,装不下的部分必然会溢出。
从低级类型向高级类型的转换,系统将自动执行,程序员无须进行任何操作。这种类型的转换称为 隐式转换,也可以称为自动转换。 下列基本数据类型会涉及数据转换(不包括逻辑类型),这些类型按精度从 “低”到“高” 排列的顺序为 byte < short < int < long < float < double,可对照下图,其中 char 类型比较特殊,它可以与部分 int 型数字兼容,且不会发生精度变化。
隐式转换具体分析如下:
Java 程序中要求参与的计算的数据,必须要保证数据类型的一致性,如果数据类型不一致将发生类型的转换。
int + int
int + long ==> long + long (把int转换成long: 从小到大,自动类型转换,不需要代码的干预)
int + long ==> int + int (把long转成int: 从大到小,强制类型转换,必须手动代码完成)
总结:
1.隐式转换(自动类型转换)概念:
取值范围小的数据或者变量可以直接赋值给取值范围大的变量(小萝卜可以直接放入大坑中)
2.特点:
(1)自动类型转换是自动完成的,不需要代码的干预
(2)byte/short/char类型数据,只要参加运算会自动转换为int类型
(3)byte、short、char–>int–>long–>float–>double
举例:有一个byte类型(1个字节)的数字5: 00000101
byte类型自动类型转换成short类型(2个字节):在左侧补充1个字节的0,因为左侧补充的都是0,对原有数据是没有影响的,仍然是5,
00000000 00000101
byte类型自动类型转换成int类型(4个字节):
在左侧补充3个字节的0,因为左侧补充的都是0,对原有数据是没有影响的,仍然是5
00000000 00000000 00000000 00000101
byte类型自动类型转换成long类型(8个字节):
在左侧补充7个字节的0,因为左侧补充的都是0,对原有数据是没有影响的,仍然是5
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
总结:根据需求,在数据前面补充若干字节的0,因为补充的都是0,对原有数据大小是没有影响的(打肿脸充胖子)
示例代码,如下:
显式转换具体分析如下:
当把高精度的变量的值赋给低精度的变量时,必须使用显式类型转换(又称强制类型转换),
当执行显式类型转换时可能会导致精度缺失。语法如下:
(类型名) 要转换的值
取值范围大的数据或者变量不能直接赋值给取值范围小的变量(大萝卜不能直接放入小坑中),解决方案:
(1) 把坑变大
(2) 把萝卜变小(强制类型转换)
2.格式:
转后类型 变量名称 = (转后类型) 转前数据或者变量;
long类型(8个字节)的数字5:
long num = 5L;
long类型强制类型转换成int类型(4个字节):
int a = (int)num;//把num中的数据强制类型转换成int类型,并把结果赋值给int变量a
举例: 有一个long类型(8个字节)的数字5:
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
long类型强制类型转换成int类型(4个字节):
砍掉左侧的四个字节的内容,因为砍掉的都是数字0,所以对最终的结果数据没有影响仍然是5
00000000 00000000 00000000 00000101
long类型强制类型转换成short类型(2个字节):
砍掉左侧的六个字节的内容,因为砍掉的都是数字0,所以对最终的结果数据没有影响仍然是5
00000000 00000101
long类型强制类型转换成byte类型(1个字节):
砍掉左侧的七个字节的内容,因为砍掉的都是数字0,所以对最终的结果数据没有影响仍然是5
00000101
总结: 根据需求,砍掉数据左侧的若干字节的数据,只要砍掉的都是0,对原数据没有影响
但是只要砍掉的数据中包含1,就会对原数据产生影响(可能会损失精度)
示例代码2,如下:
图解(其它案例):
4.5 ASCII 码表
计算机是一个二货,只能存储 0和1,所以存储到计算机中的所有内容都会转换成 0和1 进行存储。所以我们在计算机中存储的字符也不例外,也需要把字符转换成 0和1 进行存储,问题: 如何把字符转换成 0和1 呢? 通过 ASCII 编码表: 存储字符和数字对应关系的一张表格。 存储字符时:需要查找 ASCII 码表,找到字符对应的数字,将数字转换为二进制数存放到计算机中。
ASCII 码表如下图所示:
int 类型和 char 类型的运算原理,如下:
计算机中的存储单位(2的10次方就是1024)
务必记住:1个字节是8位
运算符:对常量或者变量进行操作的符号。表达式:用运算符把常量或者变量连接起来符合 java 语法的式子就可以称为表达式。
6.1 算术运算符
Java 中的算术运算符主要有 +(加号)、-(减号)、*(乘号)、/(除号)、%(求余),它们都是二元运算符。Java 中算术运算符的功能及使用方式如下表所示:
其中,“+”和“-” 运算符还可以作为数据的正负符号,如 +5、-7。说明:“+”运算符也有拼接字符串的功能。下面分别演示算术运算符的用法。
【示例1】算术运算符加减乘除。示例代码如下:
6.2 赋值运算符
赋值运算符用符号 “=” 表示,它是一个二元运算符(对两个操作数作处理),其功能是将右方操作数所含的值赋给左方的操作数。例如:
int a = 100; // 该表达式是将100赋值给变量a
/*左方的操作数必须是一个量,而右边的操作数则可以是变量(如a、number)、
常量(如123、\”book\”)、有效的表达式(如45*12)。*/
和其他主流编程语言一样,Java 中也有复合赋值运算符。所谓的复合赋值运算符,就是将赋值运算符与其他运算符合并成一个运算符来使用,从而同时实现两种运算符的效果。Java 中的复合运算符如下表所示:
以“+=”为例,虽然“a += 1”与“a = a + 1”二者最后的计算结果是相同的,但是在不同场景下,两种运算符都有各自的优势和劣势。
【示例2】赋值运算符的使用场景。示例代码如下:
6.3 自增自减运算符
自增、自减运算符是单目运算符,可以放在变量之前,也可以放在变量之后。自增、自减运算符的作用是使变量的值加1或减1。
例如下图所示:
6.4 关系运算符
关系运算符属于二元运算符,用来判断一个操作数与另外一个操作数之间的关系。不管关系表达式多么复杂或者多么简单,返回值一定是布尔类型的结果,要么是 true,要么是 false,如下表所示:
【示例3】关系运算符。示例代码如下:
6.5 逻辑运算符
假定某面包店,在每周二的下午7点至8点和每周六的下午5点至6点,对生日蛋糕商品进行折扣让利活动,那么想参加折扣活动的顾客,就要在时间上满足这样的条件,(周二并且7:00 PM~8:00 PM)或者(周六并且5:00 PM~6:00 PM),这里就用到了逻辑关系。逻辑运算符是对 true(真) 和 false(假) 这两种逻辑值进行运算,运算后的结果仍是一个逻辑值。
用来连接多个条件(布尔表达式的: 结果为true/false的式子),最终的结果也必须是一个布尔类型的数据,要么是true,要么是false
不管逻辑运算符连接的式子有多么简单或者多么复杂,最终结果要么是true,要么是false
分类:
(1) & (shift+7): 逻辑与,表示并且的意思,多个条件同时成立的意思,就是只有多个条件都是true,最终的结果才是true
特点:【有false,则false】: 只要有一个条件不成立(false),结果就是false
(2) |(shift+\\): 逻辑或,表示或者的意思,多个条件,只要有一个成立,最终的结果就是true
特点:【有true,则true】:只要有一个条件是true,结果就是true
(2) !(shift+1): 逻辑取反,!true 就是false,!false 就是true
逻辑运算符的短路效果,如下表所示:
逻辑运算符的运算结果如下表所示:
说明如下:
6.6 位运算符
位运算的操作数类型是整型,可以是有符号的,也可以是无符号的。位运算符可以分为位逻辑运算符和位移运算符两大类。
位逻辑运算符包括 &、|、^和 ~,前三个是双目运算符,第四个是单目运算符。这四个运算符的运算结果如下表所示:
参照上表来看一下这四个运算符的实际运算过程:
1)位逻辑与实际上是将操作数转换成二进制表示方式,然后将两个二进制操作数对象从低位(最右边)到高位对齐,每位求与,
若两个操作数对象同一位都为1,则结果对应位为1,否则结果中对应位为0。例如,12和8经过位逻辑与运算后得到的结果是8。
0000 0000 0000 1100 (十进制12原码表示)
& 0000 0000 0000 1000 (十进制8原码表示)
0000 0000 0000 1000 (十进制8原码表示)
2)位逻辑或实际上是将操作数转换成二进制表示方式,然后将两个二进制操作数对象从低位(最右边)到高位对齐,每位求或,
若两个操作数对象同一位都为0,则结果对应位为0,否则结果中对应位为1。例如,4和8经过位逻辑或运算后的结果是12。
0000 0000 0000 0100 (十进制4原码表示)
| 0000 0000 0000 1000 (十进制8原码表示)
0000 0000 0000 1100 (十进制12原码表示)
3)位逻辑异或实际上是将操作数转换成二进制表示方式,然后将两个二进制操作数对象从低位(最右边)到高位对齐,每位求异或,
若两个操作数对象同一位不同时,则结果对应位为1,否则结果中对应位为0。例如,31和22经过位逻辑异或运算后得到的结果是9。
0000 0000 0001 1111 (十进制31原码表示)
^ 0000 0000 0001 0110 (十进制22原码表示)
0000 0000 0000 1001 (十进制9原码表示)
4)取反运算符,实际上是将操作数转换成二进制表示方式,然后将各位二进制位由1变为0,由0变为1。
例如,123取反运算后得到的结果是-124。
~ 0000 0000 0111 1011 (十进制123原码表示)
1111 1111 1000 0100 (十进制-124原码表示)
&、| 和 ^ 也可以用于逻辑运算,运算结果如下表所示:
【示例10】位逻辑运算符的使用场景。示例代码如下:
移位运算有三个,分别是左移 <<、右移>> 和无符号右移 >>>,这三个运算符都属于双目运算符。左移是将一个二进制操作数对象按指定的移动位数向左移,左边(高位端)溢出的位被丢弃,右边(低位端)的空位用0补充。左移相当于乘以2的幂,如下图所示:
例如,short 型整数 9115 的二进制是 0010 0011 1001 1011,左移一位变成 18230,左移两位变成 -29076。
右移是将一个二进制的数按指定的位数向右移动,右边(低位端)溢出的位被丢弃,左边(高位端)用符号位补充,正数的符号位为0,负数的符号为1。右移位运算相当于除以2的幂,如下图所示:
例如 short 型整数 9115 的二进制是 0010 0011 1001 1011,右移一位变成 4557,右移两位变成 2278,运行过程如下图所示:
short 型整数 -32766 的二进制是 0010 0011 1001 1011,右移一位变成 -16383,右移两位变成 -8192,运行过程如下图所示:
无符号右移是将一个二进制的数按指定的位数向右移动,右边(低位端)溢出的位被丢弃,左边(高位端)一律用0填充,运算结果相当于除以2的幂。例如 int 型整数 -32766 的二进制是 1111 1111 1111 1111 1000 0000 0000 0010,右移一位变成 2147467265,右移两位变成 1073733632,运行过程如下图所示:
【示例11】位移运算符的使用场景。示例代码如下:
常见错误:byte、short 类型做 >>> 操作时,可能会发生数据溢出,结果仍为负数。从二进制的实现机制来说,byte 类型和 short 类型不适用于 >>> 操作。
6.7 三元运算符
1.格式:
数据类型 变量名称 = 布尔表达式1 ? 表达式2 : 表达式3;
2.执行流程:
(1)计算布尔表达式1的结果,看是true还是false
(2)如果布尔表达式1的结果为true,就把表达式2的结果赋值给左侧的变量
(3)如果布尔表达式1的结果为false,就把表达式3的结果赋值给左侧的变量
执行流程图解:
【示例13】用三元运算符完成判断一个数字的奇偶性。示例代码如下:
【示例14】使用条件表达式判断一个数是否是3和5的公倍数。示例代码如下:
图解分析:
6.8 圆括号
圆括号可以提升公式中计算过程的优先级,在编程中非常常用。如下图所示,使用圆括号更改运算的优先级,可以得到不同的结果。
圆括号还有调整代码格式,增强阅读性的功能。比如下面的这个公式:
6.9 运算符优先级
Java 中的表达式就是使用运算符连接起来并且符合 Java 规则的式子。运算符的优先级决定了表达式中运算的先后顺序。通常优先级由高到低的顺序依次是:自增和自减运算、算术运算、位运算、逻辑运算、赋值运算。如果两个运算有相同的优先级,会以从左到右的方式进行运算。下表显示 Java 中的运算符的优先级。
今日份分享已结束,请大家多多包涵和指点!
揭秘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居民们,让我们一起遵守这些神圣的编码准则吧。就像穿上了超级英雄的紧身衣,让我们的代码更加健壮、优雅,并且……当然,更加酷炫!
记住,好的编码规范不仅能让你的代码“活”得更久,还能让后来者在阅读你的代码时,不至于想要穿越时空来找你算账。最后,让我们共勉之——在代码的世界里,每一个规范的遵守,都是向着成为编程界超级英雄迈出的一步。
你见过哪些实用到爆的 Java 代码技巧?
专注于Java领域优质技术,欢迎关注
作者:java小瓜哥
自从毕业后,今年已经是我工作的第 8 个年头了,我甚至都快忘记了到底是那年毕业的。从出来本人一直在做 Java 相关的工作,现在终于有时间坐下来,写一篇关于 Java 写法的一篇文章,来探讨一下如果你真的是一个 Java 程序员,那你真的会写 Java 吗?
笔者是一个务实的程序员,故本文绝非扯淡文章,文中内容都是干货,望读者看后,能有所收获。
本文不是一个吹嘘的文章,不会讲很多高深的架构,相反,会讲解很多基础的问题和写法问题,如果读者自认为基础问题和写法问题都是不是问题,那请忽略这篇文章,节省出时间去做一些有意义的事情。
不知道有多少”老”程序员还在使用 Eclipse,这些程序员们要不就是因循守旧,要不就是根本就不知道其他好的开发工具的存在,Eclipse 吃内存卡顿的现象以及各种偶然莫名异常的出现,都告知我们是时候寻找新的开发工具了。
更换 IDE
根本就不想多解释要换什么样的 IDE,如果你想成为一个优秀的 Java 程序员,请更换 IntelliJ IDEA。使用 IDEA 的好处,请搜索谷歌。
别告诉我快捷键不好用
更换 IDE 不在我本文的重点内容中,所以不想用太多的篇幅去写为什么更换IDE。在这里,我只能告诉你,更换 IDE 只为了更好、更快的写好 Java 代码。原因略。
别告诉我快捷键不好用,请尝试新事物。
bean
bean 使我们使用最多的模型之一,我将以大篇幅去讲解 bean,希望读者好好体会。
domain 包名
根据很多 Java 程序员的”经验”来看,一个数据库表则对应着一个 domain 对象,所以很多程序员在写代码时,包名则使用:com.xxx.domain ,这样写好像已经成为了行业的一种约束,数据库映射对象就应该是 domain。但是你错了,domain 是一个领域对象,往往我们再做传统 Java 软件 Web 开发中,这些 domain 都是贫血模型,是没有行为的,或是没有足够的领域模型的行为的,所以,以这个理论来讲,这些 domain 都应该是一个普通的 entity 对象,并非领域对象,所以请把包名改为:com.xxx.entity。
如果你还不理解我说的话,请看一下 Vaughn Vernon 出的一本叫做《IMPLEMENTING DOMAIN-DRIVEN DESIGN》(实现领域驱动设计)这本书,书中讲解了贫血模型与领域模型的区别,相信你会受益匪浅。
DTO
数据传输我们应该使用 DTO 对象作为传输对象,这是我们所约定的,因为很长时间我一直都在做移动端 API 设计的工作,有很多人告诉我,他们认为只有给手机端传输数据的时候(input or output),这些对象成为 DTO 对象。请注意!这种理解是错误的,只要是用于网络传输的对象,我们都认为他们可以当做是 DTO 对象,比如电商平台中,用户进行下单,下单后的数据,订单会发到 OMS 或者 ERP 系统,这些对接的返回值以及入参也叫 DTO 对象。
我们约定某对象如果是 DTO 对象,就将名称改为 XXDTO,比如订单下发OMS:OMSOrderInputDTO。
DTO 转化
正如我们所知,DTO 为系统与外界交互的模型对象,那么肯定会有一个步骤是将 DTO 对象转化为 BO 对象或者是普通的 entity 对象,让 service 层去处理。
场景
比如添加会员操作,由于用于演示,我只考虑用户的一些简单数据,当后台管理员点击添加用户时,只需要传过来用户的姓名和年龄就可以了,后端接受到数据后,将添加创建时间和更新时间和默认密码三个字段,然后保存数据库。
我们只关注一下上述代码中的转化代码,其他内容请忽略:
请使用工具
上边的代码,从逻辑上讲,是没有问题的,只是这种写法让我很厌烦,例子中只有两个字段,如果有 20 个字段,我们要如何做呢? 一个一个进行 set 数据吗?当然,如果你这么做了,肯定不会有什么问题,但是,这肯定不是一个最优的做法。
网上有很多工具,支持浅拷贝或深拷贝的 Utils。举个例子,我们可以使用 org.springframework.beans.BeanUtils#copyProperties 对代码进行重构和优化:
BeanUtils.copyProperties 是一个浅拷贝方法,复制属性时,我们只需要把 DTO 对象和要转化的对象两个的属性值设置为一样的名称,并且保证一样的类型就可以了。如果你在做 DTO 转化的时候一直使用 set 进行属性赋值,那么请尝试这种方式简化代码,让代码更加清晰!
转化的语义
上边的转化过程,读者看后肯定觉得优雅很多,但是我们再写 Java 代码时,更多的需要考虑语义的操作,再看上边的代码:
虽然这段代码很好的简化和优化了代码,但是他的语义是有问题的,我们需要提现一个转化过程才好,所以代码改成如下:
这是一个更好的语义写法,虽然他麻烦了些,但是可读性大大增加了,在写代码时,我们应该尽量把语义层次差不多的放到一个方法中,比如:
这两段代码都没有暴露实现,都是在讲如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。
如上所述,是一种重构方式,读者可以参考 Martin Fowler 的《Refactoring Imporving the Design of Existing Code》(重构 改善既有代码的设计) 这本书中的 Extract Method 重构方式。
抽象接口定义
当实际工作中,完成了几个 API 的 DTO 转化时,我们会发现,这样的操作有很多很多,那么应该定义好一个接口,让所有这样的操作都有规则的进行。
如果接口被定义以后,那么 convertFor 这个方法的语义将产生变化,它将是一个实现类。
看一下抽象后的接口:
虽然这个接口很简单,但是这里告诉我们一个事情,要去使用泛型,如果你是一个优秀的 Java 程序员,请为你想做的抽象接口,做好泛型吧。
我们再来看接口实现:
我们这样重构后,我们发现现在的代码是如此的简洁,并且那么的规范:
review code
如果你是一个优秀的 Java 程序员,我相信你应该和我一样,已经数次重复 review 过自己的代码很多次了。
我们再看这个保存用户的例子,你将发现,API 中返回值是有些问题的,问题就在于不应该直接返回 User 实体,因为如果这样的话,就暴露了太多实体相关的信息,这样的返回值是不安全的,所以我们更应该返回一个 DTO 对象,我们可称它为 UserOutputDTO:
这样你的 API 才更健全。
不知道在看完这段代码之后,读者有是否发现还有其他问题的存在,作为一个优秀的 Java 程序员,请看一下这段我们刚刚抽象完的代码:
你会发现,new 这样一个 DTO 转化对象是没有必要的,而且每一个转化对象都是由在遇到 DTO 转化的时候才会出现,那我们应该考虑一下,是否可以将这个类和 DTO 进行聚合呢,看一下我的聚合结果:
然后 API 中的转化则由:
变成了:
我们再 DTO 对象中添加了转化的行为,我相信这样的操作可以让代码的可读性变得更强,并且是符合语义的。
再查工具类
再来看 DTO 内部转化的代码,它实现了我们自己定义的 DTOConvert 接口,但是这样真的就没有问题,不需要再思考了吗?
我觉得并不是,对于 Convert 这种转化语义来讲,很多工具类中都有这样的定义,这中 Convert 并不是业务级别上的接口定义,它只是用于普通 bean 之间转化属性值的普通意义上的接口定义,所以我们应该更多的去读其他含有 Convert 转化语义的代码。
我仔细阅读了一下 GUAVA 的源码,发现了 com.google.common.base.Convert 这样的定义:
从源码可以了解到,GUAVA 中的 Convert 可以完成正向转化和逆向转化,继续修改我们 DTO 中转化的这段代码:
修改后:
看了这部分代码以后,你可能会问,那逆向转化会有什么用呢?其实我们有很多小的业务需求中,入参和出参是一样的,那么我们变可以轻松的进行转化,我将上边所提到的 UserInputDTO 和 UserOutputDTO 都转成 UserDTO 展示给大家。
DTO:
API:
当然,上述只是表明了转化方向的正向或逆向,很多业务需求的出参和入参的 DTO 对象是不同的,那么你需要更明显的告诉程序:逆向是无法调用的:
看一下 doBackward 方法,直接抛出了一个断言异常,而不是业务异常,这段代码告诉代码的调用者,这个方法不是准你调用的,如果你调用,我就”断言”你调用错误了。
关于异常处理的更详细介绍,可以参考这篇文章:如何优雅的设计 Java 异常(lrwinx.github.io/2016/04/28/…) ,应该可以帮你更好的理解异常。
bean 的验证
如果你认为我上边写的那个添加用户 API 写的已经非常完美了,那只能说明你还不是一个优秀的程序员。我们应该保证任何数据的入参到方法体内都是合法的。
为什么要验证
很多人会告诉我,如果这些 API 是提供给前端进行调用的,前端都会进行验证啊,你为什还要验证?
其实答案是这样的,我从不相信任何调用我 API 或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如 Charles 进行抓包),直接将数据传入到我的 API,那我仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!
“对于脏数据的产生一定是致命”,这句话希望大家牢记在心,再小的脏数据也有可能让你找几个通宵!
jsr 303验证
hibernate 提供的 jsr 303 实现,我觉得目前仍然是很优秀的,具体如何使用,我不想讲,因为谷歌上你可以搜索出很多答案!
再以上班的 API 实例进行说明,我们现在对 DTO 数据进行检查:
API 验证:
我们需要将验证结果传给前端,这种异常应该转化为一个 api 异常(带有错误码的异常)。
BindingResult 是 Spring MVC 验证 DTO 后的一个结果集,可以参考spring 官方文档(spring.io/)。
检查参数后,可以抛出一个“带验证码的验证错误异常”
具体可以参考这篇:很优秀的文章
http://lrwinx.github.io/2016/04/28/%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E7%9A%84%E8%AE%BE%E8%AE%A1java%E5%BC%82%E5%B8%B8/
拥抱 lombok
上边的 DTO 代码,已经让我看的很累了,我相信读者也是一样,看到那么多的 Getter 和 Setter 方法,太烦躁了,那时候有什么方法可以简化这些呢。
请拥抱 lombok,它会帮助我们解决一些让我们很烦躁的问题
去掉 Setter 和 Getter
其实这个标题,我不太想说,因为网上太多,但是因为很多人告诉我,他们根本就不知道 lombok 的存在,所以为了让读者更好的学习,我愿意写这样一个例子:
看到了吧,烦人的 Getter 和 Setter 方法已经去掉了。
但是上边的例子根本不足以体现 lombok 的强大。我希望写一些网上很难查到,或者很少人进行说明的 lombok 的使用以及在使用时程序语义上的说明。
比如:@Data,@AllArgsConstructor,@NoArgsConstructor..这些我就不进行一一说明了,请大家自行查询资料。
bean 中的链式风格
什么是链式风格?我来举个例子,看下面这个 Student 的 bean:
仔细看一下 set 方法,这样的设置便是 chain 的 style,调用的时候,可以这样使用:
相信合理使用这样的链式代码,会更多的程序带来很好的可读性,那看一下如果使用 lombok 进行改善呢,请使用 @Accessors(chain = true),看如下代码:
这样就完成了一个对于 bean 来讲很友好的链式操作。
静态构造方法
静态构造方法的语义和简化程度真的高于直接去 new 一个对象。比如 new 一个 List 对象,过去的使用是这样的:
看一下 guava 中的创建方式:
Lists 命名是一种约定(俗话说:约定优于配置),它是指 Lists 是 List 这个类的一个工具类,那么使用 List 的工具类去产生 List,这样的语义是不是要比直接 new 一个子类来的更直接一些呢,答案是肯定的,再比如如果有一个工具类叫做 Maps,那你是否想到了创建 Map 的方法呢:
好了,如果你理解了我说的语义,那么,你已经向成为 Java 程序员更近了一步了。
再回过头来看刚刚的 Student,很多时候,我们去写 Student 这个 bean 的时候,他会有一些必输字段,比如 Student 中的 name 字段,一般处理的方式是将 name 字段包装成一个构造方法,只有传入 name 这样的构造方法,才能创建一个 Student 对象。
接上上边的静态构造方法和必传参数的构造方法,使用 lombok 将更改成如下写法(@RequiredArgsConstructor 和 @NonNull):
测试代码:
这样构建出的 bean 语义是否要比直接 new 一个含参的构造方法(包含 name 的构造方法)要好很多。
当然,看过很多源码以后,我想相信将静态构造方法 ofName 换成 of 会先的更加简洁:
测试代码:
当然他仍然是支持链式调用的:
这样来写代码,真的很简洁,并且可读性很强。
使用 builder
Builder 模式我不想再多解释了,读者可以看一下《Head First》(设计模式) 的建造者模式。
今天其实要说的是一种变种的 builder 模式,那就是构建 bean 的 builder 模式,其实主要的思想是带着大家一起看一下 lombok 给我们带来了什么。
看一下 Student 这个类的原始 builder 状态:
调用方式:
这样的 builder 代码,让我是在恶心难受,于是我打算用 lombok 重构这段代码:
调用方式:
代理模式
正如我们所知的,在程序中调用 rest 接口是一个常见的行为动作,如果你和我一样使用过 spring 的 RestTemplate,我相信你会我和一样,对他抛出的非 http 状态码异常深恶痛绝。
所以我们考虑将 RestTemplate 最为底层包装器进行包装器模式的设计:
然后再由扩展类对 FilterRestTemplate 进行包装扩展:
包装器 ExtractRestTemplate 很完美的更改了异常抛出的行为,让程序更具有容错性。在这里我们不考虑 ExtractRestTemplate 完成的功能,让我们把焦点放在 FilterRestTemplate 上,“实现 RestOperations 所有的接口”,这个操作绝对不是一时半会可以写完的,当时在重构之前我几乎写了半个小时,如下:
我相信你看了以上代码,你会和我一样觉得恶心反胃,后来我用 lombok 提供的代理注解优化了我的代码(@Delegate):
这几行代码完全替代上述那些冗长的代码。
是不是很简洁,做一个拥抱 lombok 的程序员吧。
需求案例
项目需求
项目开发阶段,有一个关于下单发货的需求:如果今天下午 3 点前进行下单,那么发货时间是明天,如果今天下午 3 点后进行下单,那么发货时间是后天,如果被确定的时间是周日,那么在此时间上再加 1 天为发货时间。
思考与重构
我相信这个需求看似很简单,无论怎么写都可以完成。
很多人可能看到这个需求,就动手开始写 Calendar 或 Date 进行计算,从而完成需求。
而我给的建议是,仔细考虑如何写代码,然后再去写,不是说所有的时间操作都用 Calendar 或 Date 去解决,一定要看场景。
对于时间的计算我们要考虑 joda-time 这种类似的成熟时间计算框架来写代码,它会让代码更加简洁和易读。
请读者先考虑这个需求如何用 Java 代码完成,或先写一个你觉得完成这个代码的思路,再来看我下边的代码,这样,你的收获会更多一些:
读这段代码的时候,你会发现,我将判断和有可能出现的不同结果都当做一个变量,最终做一个三目运算符的方式进行返回,这样的优雅和可读性显而易见,当然这样的代码不是一蹴而就的,我优化了 3 遍产生的以上代码。读者可根据自己的代码和我写的代码进行对比。
提高方法
如果你做了 3 年+的程序员,我相信像如上这样的需求,你很轻松就能完成,但是如果你想做一个会写 Java 的程序员,就好好的思考和重构代码吧。
写代码就如同写字一样,同样的字,大家都会写,但是写出来是否好看就不一定了。如果想把程序写好,就要不断的思考和重构,敢于尝试,敢于创新,不要因循守旧,一定要做一个优秀的 Java 程序员。
提高代码水平最好的方法就是有条理的重构!(注意:是有条理的重构)
设计模式
设计模式就是工具,而不是提现你是否是高水平程序员的一个指标。
我经常会看到某一个程序员兴奋的大喊,哪个程序哪个点我用到了设计模式,写的多么多么优秀,多么多么好。我仔细去翻阅的时候,却发现有很多是过度设计的。
业务驱动技术 or 技术驱动业务
业务驱动技术 or 技术驱动业务 ? 其实这是一个一直在争论的话题,但是很多人不这么认为,我觉得就是大家不愿意承认罢了。我来和大家大概分析一下作为一个 Java 程序员,我们应该如何判断自己所处于的位置.
业务驱动技术:如果你所在的项目是一个收益很小或者甚至没有收益的项目,请不要搞其他创新的东西,不要驱动业务要如何如何做,而是要熟知业务现在的痛点是什么?如何才能帮助业务盈利或者让项目更好,更顺利的进行。
技术驱动业务:如果你所在的项目是一个很牛的项目,比如淘宝这类的项目,我可以在满足业务需求的情况下,和业务沟通,使用什么样的技术能更好的帮助业务创造收益,比如说下单的时候要进队列,可能几分钟之后订单状态才能处理完成,但是会让用户有更流畅的体验,赚取更多的访问流量,那么我相信业务愿意被技术驱动,会同意订单的延迟问题,这样便是技术驱动业务。
我相信大部分人还都处于业务驱动技术的方向吧。
所以你既然不能驱动业务,那就请拥抱业务变化吧。
代码设计
一直在做 Java 后端的项目,经常会有一些变动,我相信大家也都遇到过。
比如当我们写一段代码的时候,我们考虑将需求映射成代码的状态模式,突然有一天,状态模式里边又添加了很多行为变化的东西,这时候你就挠头了,你硬生生的将状态模式中添加过多行为和变化。
慢慢的你会发现这些状态模式,其实更像是一簇算法,应该使用策略模式,这时你应该已经晕头转向了。
说了这么多,我的意思是,只要你觉得合理,就请将状态模式改为策略模式吧,所有的模式并不是凭空想象出来的,都是基于重构。
Java 编程中没有银弹,请拥抱业务变化,一直思考重构,你就有一个更好的代码设计!
你真的优秀吗?
真不好意思,我取了一个这么无聊的标题。
国外流行一种编程方式,叫做结对编程,我相信国内很多公司都没有这么做,我就不在讲述结对编程带来的好处了,其实就是一边 code review,一边互相提高的一个过程。既然做不到这个,那如何让自己活在自己的世界中不断提高呢?
“平时开发的时候,做出的代码总认为是正确的,而且写法是完美的。”,我相信这是大部分人的心声,还回到刚刚的问题,如何在自己的世界中不断提高呢?
答案就是:
- 多看成熟框架的源码
- 多回头看自己的代码
- 勤于重构
你真的优秀吗? 如果你每周都完成了学习源码,回头看自己代码,然后勤于重构,我认为你就真的很优秀了。
即使也许你只是刚刚入门,但是一直坚持,你就是一个真的会写java代码的程序员了。
UML
不想多讨论 UML 相关的知识,但是我觉得你如果真的会写 Java,请先学会表达自己,UML 就是你说话的语言,做一名优秀的 Java 程序员,请至少学会这两种 UML 图:
- 类图
- 时序图
clean code
我认为保持代码的简洁和可读性是代码的最基本保证,如果有一天为了程序的效率而降低了这两点,我认为是可以谅解的,除此之外,没有任何理由可以让你任意挥霍你的代码。
- 读者可以看一下 Robert C. Martin 出版的《Clean Code》(代码整洁之道) 这本书
- 可以参考美团文章聊聊 clean code(tech.meituan.com/clean-code.…);
- 也可以看一下阿里的 Java 编码规范(yq.aliyun.com/articles/69…)。
无论如何,请保持你的代码的整洁。
Linux 基础命令
这点其实和会写 Java 没有关系,但是 Linux 很多时候确实承载运行 Java 的容器,请学好 Linux 的基础命令。
- 参考鸟哥的《Linux私房菜》
Java 是一个大体系,今天讨论并未涉及框架和架构相关知识,只是讨论如何写好代码。
本文从写 Java 程序的小方面一直写到大方面,来阐述了如何才能写好 Java 程序,并告诉读者们如何才能提高自身的编码水平。
我希望看到这篇文章的各位都能做一个优秀的 Java 程序员
来源:掘金 链接:https://juejin.im/post/5d8db248f265da5b81793861
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。