如何编写优雅的 Java 代码
不知道各位在项目开发过程中有没有过这种体会,接手上一任的代码,看到代码的那一刻,有一种想要砸电脑的冲动,一个方法体内写了无数行代码,到处皆可看到复制粘贴的代码,变量命名也让人看不懂。
各位在编码时,是否有想过,我如何才能写出高质量的代码,写出优雅的代码,写出高度可扩展的代码。我相信大家都希望能写出那样的代码,让人佩服的五体投地,可不知道如何写,那么本文就是为帮助大家提高编码质量而生的。
我相信,大家在看完本文后,一定会有所领悟。下面,我们就进入主题。
在介绍 lombok 之前,我们先来看一段代码:
这段代码大家应该都很熟悉,我们在开发 JavaWeb 项目时,定义一个 Bean,会先写好属性,然后设置 getter/setter 方法,这段代码本身没有任何问题,也必须这样写。
但是每个 bean 都需要些 getter/setter,这样写的话就不够优雅了,这段代码我们如何优雅的写呢?接下来就轮到强大的 lombok 出场了。
lombok 是一个可以通过注解的形式来简化我们的代码的一个插件,要使用它,我们应该先安装插件,安装步骤如下:
1.
打开 IDEA 的 Plugins,点击 Browse repositories。
2.
搜索 lombok,并安装它,安装好后重启 IDEA。
3.
打开 Settings,找到 Annotaion Processors,将 Enable annotaion processing 勾选上。
4.单纯这样还不够,我们要用到 lombok 的注解还需要添加其依赖:
我们可以看到,在类上加入了 Getter 和 Setter 两个注解,将之前写的 getter/setter 方法干掉了,这种代码看着清爽多了,写个 main 方法来测试下:
我们并没有写任何 setter/getter 方法,只是加了两个注解就可以调用了,这是为什么呢?这是因为 lombok 提供的 Getter 和 Setter 注解是编译时注解,也就是在编译时,lombok 会自动为我们添加 getter/setter 方法,因此我们不需要显式地去写 getter/setter 方法而可以直接调用,这样的代码是不是看着非常优雅。
当然,lombok 的功能不止于此,它提供了很多注解以简化我们的代码,下面,我将分别介绍它的其他常用注解:
该注解的作用是是否开启链式调用,比如我们开启链式调用:
构建者模式,我们在使用第三方框架时经常能看到 Builder 模式,比如 HttpClient 的:
那么,通过 Builder 注解可以很方便的实现它:
编译时添加 getter、setter、toString、equals 和 hashCode 方法,如:
添加输入输出流 close 方法,如:
我们通过断点调试发现,它会调用 close(),说明 lombok 帮我们生成了 close():
通过 lombok 的注解,可以极大的减少我们的代码量,并且更加清爽,更加优雅。
lombok 还有很多注解,如:
- @ToString: 生成 toString() 方法。
- @EqualsAndHashCode: 生成 equals 和 hashCode 方法。
日志相关注解(当然需要添加相关日志依赖):
- Slf4j、Log4j 等。
- NoArgsConstructor、AllArgsConstructor: 生成无参和全参构造函数。
- Value: 生成 getter、toString、equals、hashCode 和全参构造函数。
- NonNull: 标注在字段和方法上,表示非空,会在第一次使用时判断。
一说到设计模式,我相信大家都能说出那 23 种设计模式,并且还能说出每种设计模式的用法,但是大多数同学应该都没有在实际应用中真正运用过设计模式,还只是停留在理论阶段。
不知道各位是否有过这个感觉,整个应用被相同的代码充斥着,自己也知道这种代码不好,但是不知道怎么做优化,虽然知道有 23 种设计模式,却不知道怎么运用。
本节我就将以实际的例子教大家如何在实际应用中灵活运用所学的设计模式。
(实际应用中,一个场景可能不只包含一个设计模式,很有可能需要多种设计模式配合使用才能写出优雅的高质量的代码。)
我们在做后台管理系统时,会有这样一个需求,根据后台的数据统计导出报表,需要支持 Excel、Word、PPT、PDF 等格式。
对于以上需求,一般做法是:为每一个导出报表的方法提供一个方法,然后在 Service 里判断,如果为 excel,则调用 excel 的方法,如果为 Word 则调用 word 的方法,
如:
这样写本身没有问题,也能实现需求,但是它有以下缺点:
- 1.代码不够优雅,业务方法内存在太多 if – else。
- 2.扩展性不强,每增加一个报表格式,就需要修改业务方法,增加一个 if – else。
我们在开发时需要遵循有一个原则:一个方法做你该做的事。也就是无论增加什么样的报表格式,业务方法 exportReport 的作用依然是导出功能,除非业务需求发生改变,否则不能修改业务方法。
那么,我们该怎么改造呢?
我们发现,导出报表可以导出不同的格式,这些格式我们可以理解为产品,需要由一个地方产出,因此马上就能想到可以利用工厂模式对其进行改造,下面是改造后的代码:
这样就完成了工厂模式对报表导出的改造,在业务方法内,通过 TemplateFactory 创建 template,然后调用 template 的 read 或 write 方法,以后我们每增加一个格式,只需要实现 Template 的相应方法,在 factory 实例化它即可,无需修改业务方法。
此场景用到的设计模式有:简单工厂模式。
这种场景也比较多见,比如:
- 1.我们实现一个注册功能,注册的字段比较多,可能就会分步骤进行,第一步,填写手机号验证码,第二步,填写头像昵称。
- 2.我们发布一篇文章,第一步,填写标题和内容,第二步,设置定时任务,第三步,设置文章打赏规则。
针对这些情况,一般做法也是在业务方法内,做个 if – else 判断,如果是第一步,则执行第一步的业务,如果是第二步,则执行第二步的业务,这种方式同场景 1 一样,代码也比较难看。
对于这样的场景,我们同样可以使用设计模式来实现,因为每一步都是有关联的,执行完第一步,才能执行第二步,执行完第二步才能执行第三步,它很像一条链子将它们联系起来,所以很容易想到可以采用责任链模式。
下面,请看具体的实现:
业务类传入一个 step,通过 HandlerFactory 实例化 handler,通过 handler 就可以执行指定的步骤,同样地,增加一个步骤,业务类无需任何变动。
有些时候,我们会使用多重循环,直接带业务方法里写,看着很不优雅,就像这样:
我们可以将其进行封装改造,将循环细节封装起来,只将一些方法暴露给业务方调用:
上面的 main 方法就是我们业务调用时需要调用的方法,可以看出,我们将循环细节封装到 Lists 里面,使调用方的代码更加优雅。
此场景用到的设计模式有:构建者模式、观察者模式。
在实际应用中,我们看到最多的代码便是 if – else,这样的代码在业务场景中出现太多的话,看着就不太优雅了,前面的场景其实已经多次将 if – else 用设计模式替换,本场景,我将会用新的设计模式来替换讨厌的 if – else,那就是策略模式。
策略模式,通俗点讲,就是根据不同的情况,采取不同的策略,我们把它转化成 if – else,即:
这样,我们就避免了在业务场景中大量地使用 if – else 了。
通过以上的学习,我们其实是可以写出很多优雅的代码,各位在实际中如果有什么问题,或者在实际应用中发现一段代码不知道如何优化也可以再本 chat 的读者圈随时向我提问。
接下来,我将告诉大家一些 Java 编程的小技巧,利用这些技巧,可以避免一些低级 bug,也可以写出一些优雅的代码。
我们在集成一个类时,可能会重写父类方法,大家务必加上 Override 注解,请看下面的代码:
我们的本意是要重写 method,但是参数类型写错了,变成了重载,编译器不会报错,如果我们加上 @Override,编译器会报错,我们就能马上发现代码的错误,而避免运行一段时间导致的 bug。
为什么这么说呢?错误我们马上就能发现,而且如果是编译时错误,都无法运行,但是警告并不影响编译和运行,举个例子:
我的本意是 for 循环用 i,但是却写成了 j,这时编译不会报错,但是 IDE 会给出警告:
它告诉我们i这个变量没有使用到,如果忽略警告,那么很可能运行一段时间出现致命性的 bug,但是如果我们重视警告,当编译器提出这个警告时,我们就会想,i为什么没有用到呢,检查代码,马上就能发现隐藏的 bug。
在开发数据库项目时,经常会有一些譬如状态、类型、性别等具有固定值的字段,一般我们会用数字表示,在业务中,也会经常判断,比如状态为 1 时,执行什么操作,如果直接这样写数字,必须要写注释,否则很难懂。类似这种字段,尽量封装成枚举类型,如:
我们在使用时直接调用枚举,可读性增加了,也利于扩展。
小王是公司的 Android 开发工程师,在开发应用时,封装了一些常量,用于提示语。
架构师在 code review 时发现,变量命名很成问题,如:
架构师要求小王更正,但小王给我的理由是这种编码是产品经理定的,我可以在每个调用者上面加上注释,而保留现状。
很明显,这样的代码是不可取的,如果换成一个可读变量名是不是更清晰呢?比如:
这个原则很好理解,即一个方法只做一件事,如果一个方法做了太多的事,请考虑重构此方法,合理运用类似上面提到的设计模式。
下面对两种代码进行比较:
如果变量放在前面,一旦变量为 null,则会出现空指针异常,但是常量放在前面,则不会出现空指针异常。
各位看到网上经常再说,位运算效率高怎么怎么样的,但事实真的如此吗,我们不妨做个测试:
以上代码,我分别测试了 1 万次,10 万次和 100 万次,得出的结论是 1 万次速度一样,10 万次和 100 万次都只相差 2 毫秒,如今计算机计算性能越来越好,利用位运算和四则运算效率相差太小,而位运算的可读性非常低,除非有详细的注释,否则一般人真看不懂。
因此,尽量少用位运算。当然有些场景是避免不了的,比如:密码生成、图像处理等,但实际应用中,我们很少自己写这类算法。
我们如果要精确计算浮点数,切记不要用 float 和 double,它们的计算结果往往不是你想要的,比如:
计算结果为:
我们要精确计算,需要用 BigDecimal 类,如:
这样就能得出精确的值:
java8 为我们带来了 lambda 表达式,也带来了集合的流式运算,java8 以前,我们循环集合是这样的:
java8 以后,我们可以这样做:
通过集合的流式操作,我们可以很方便的过滤元素、分组、排序等,如:
除了,集合的流式操作,通过 lambda 表示,我们还可以实例化匿名类,如:
可以看出,使用 lambda 表达式,让我们的代码更加简洁,也更加优雅,同学们,请拥抱 lambda 吧!
Java注解最全详解(超级详细)
Java注解是一个很重要的知识点,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。
掌握好Java注解有利于学习框架底层实现。@mikechen
Java注解又称Java标注,是在 JDK5 时引入的新特性,注解(也被称为元数据)。
Java注解它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。
Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。
1.生成文档这是最常见的,也是java 最早提供的注解;
2.在编译时进行格式检查,如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;
3.跟踪代码依赖性,实现替代配置文件功能,比较常见的是spring 2.5 开始的基于注解配置,作用就是减少配置;
4.在反射的 Class, Method, Field 等函数中,有许多于 Annotation 相关的接口,可以在反射中解析并使用 Annotation。
包括@Override、@Deprecated、@SuppressWarnings等,使用这些注解后编译器就会进行检查。
元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等。元注解也是Java自带的标准注解,只不过用于修饰注解,比较特殊。
用户可以根据自己的需求定义注解。
JDK 中内置了以解:
如果试图使用 @Override 标记一个实际上并没有覆写父类的方法时,java 编译器会告警。
@SuppressWarnings 用于关闭对类、方法、成员编译时产生的特定警告。
1)抑制单类型的警告
2)抑制多类型的警告
3)抑制所有类型的警告
@SuppressWarnings 注解的常见参数值的简单说明:
@FunctionalInterface 用于指示被修饰的接口是函数式接口,在 JDK8 引入。
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
元注解是java API提供的,是用于修饰注解的注解,通常用在注解的定义上:
@ Retention用来定义该注解在哪一个级别可用,在源代码中(SOURCE)、类文件中(CLASS)或者运行时(RUNTIME)。
@Retention 源码:
RetentionPolicy 是一个枚举类型,它定义了被 @Retention 修饰的注解所支持的保留级别:
@Documented:生成文档信息的时候保留注解,对类作辅助说明
@Documented 示例
@Target:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
@Target源码:
ElementType 是一个枚举类型,它定义了被 @Target 修饰的注解可以应用的范围:
@Inherited:说明子类可以继承父类中的该注解
表示自动继承注解类型。 如果注解类型声明中存在 @Inherited 元注解,则注解所修饰类的所有子类都将会继承此注解。
@Repeatable 表示注解可以重复使用。
当我们需要重复使用某个注解时,希望利用相同的注解来表现所有的形式时,我们可以借助@Repeatable注解。以 Spring @Scheduled 为例:
如果不满足于文章详解,私信【架构】获取视频详解!
如何写出同事看不懂的Java代码?
原创:微信公众号 码农参上,欢迎分享,转载请保留出处。
哈喽大家好啊,我是没更新就是在家忙着带娃的Hydra。
前几天,正巧赶上组里代码review,一下午下来,感觉整个人都血压拉满了。五花八门的代码让我不禁感叹,代码规范这条道路还是任重而道远…
那么今天就来给大家总结一波Java中的代码作死小技巧,熟练掌握这些小技巧后,保证能让你写出同事看不懂的代码~
至于为啥要写出同事看不懂的代码,通过这次教训,我发现好处还是挺多的,简单举几个例子:
- 同事无法轻易修改你的代码,避免团队协作不当引入bug
- 塑造个人能力的不可替代性,规避被辞退的风险
- 代码review时,帮助同事治疗好多年的低血压
好了,一本正经的胡说八道环节就此打住……废话不多说了,下面正式开始。没用的知识又要增加了…
我打赌你肯定想不到,有人居然会在注释里下了毒。看看下面的代码,简单到main方法中只有一行注释。
猜猜看,这段程序运行结果如何?执行后它居然会在控制台打印:
看到这你是不是一脸懵逼,为什么注释中的代码会被执行?
其实原理就在于大家熟悉的unicode编码,上面的\\u000d就是一个unicode转义字符,它所表示的是一个换行符。而java中的编译器,不仅会编译代码,还会解析unicode编码将它替换成对应的字符。所以说,上面的代码解析完后实际是这样的:
这样,就能解释为什么能够执行注释中的语句了。当然,如果你觉得上面的代码不够绝,想要再绝一点,那么就可以把代码写成下面这个样子。
执行结果会打印2,同理,因为后面的unicode编码的转义后表示的是a++;。
至于这么写有什么好处,当然是用在某些不想让别人看懂的地方,用来掩人耳目了,估计大家都看过下面这个笑话。
你这么写的话客户如果懂点代码,看一下就穿帮了啊,但是你如果写成下面这样,大部分估计都以为这是一段乱码:
恕我直言,没个几十年的功力真看不出来这里执行的是sleep,简直完美。
要想写出别人看不懂的代码,很重要的一个小技巧就是把简单的东西复杂化。例如,判断一个int型数字的正负时明明可以写成这样:
但是我偏不,放着简单的代码不用,我就是玩,非要写成下面这样:
怎么样,这么写的话是不是一下子就支棱起来了!别人看到这多少得琢磨一会这块到底写了个啥玩意。
其实原理也很简单,这里用到的>>>是无符号右移操作。举个简单的例子,以-3为例,移位前先转化为它的补码:
无符号右移一位后变成下面的形式,这个数转化为十进制后是2147483646。
所以,当一个int类型的数字在无符号右移31位后,其实在前面的31位高位全部是0,剩下的最低位是原来的符号位,因此可以用来判断数字的正负。
基于这个小知识,我们还能整出不少活来。例如,放着好好的0不用,我们可以通过下面的方式定义一个0:
通过上面的知识,相信大家可以轻易理解,因为在将一个数字无符号右移32位后,二进制的所有位上全部是0,所以最终会得到0。那么问题来了,我为什么不直接用Integer.MAX_VALUE>>32,一次性右移32位呢?
这是因为在对int型的数字进行移位操作时,会对操作符右边的参数进行模32的取余运算,因此如果直接写32的话,那么相当于什么都不做,得到的还是原数值。
古有赵高指鹿为马,今有码农颠倒真假。阻碍同事阅读你代码的有力武器之一,就是让他在遇到条件判断时失去基本判断能力,陷入云里雾里,不知道接下来要走的是哪一个分支。
下面的代码,我说会打印fasle,是不是没有人会信?
没错,只要大家了解布尔类型就知道这不符合逻辑,但是,经过下面的改造就可以让它变为现实。
首先,在类中找个隐蔽的位置插入下面这段代码:
然后再运行上面的程序,你就会发现神奇地打印了false。
其实原理也很简单,首先通过反射拿到Boolean类中定义的TRUE这个变量:
接着使用反射,去掉它的final修饰符,最后再将它的值设为false。而在之后再使用true进行定义Boolean类型的变量过程中,会进行自动装箱,调用下面的方法:
这时的b为true,而TRUE实际上是false,因此不满足第一个表达式,最终会返回false。
这样一来就能解释上面的打印结果了,不过切记,这么写的时候一定要找一个代码中隐蔽的角落,不要被人发现,否则容易被打的很惨…
接下来要介绍的这个技巧就有点厉害了,可以将原有的一段串行逻辑改写成判断逻辑中的不同分支,并且保证最后能够正常执行。
在开始前先提一个问题,有没有一种方法,可以让if和else中的语句都能执行,就像下面的这个例子中:
如果我说只调用一次这个方法,就能同时输出if和else中的打印语句,你肯定会说不可能,因为这违背了java中判断逻辑的基本常识。
没错,在限定了上面的修饰语只调用『一次』方法的条件下,谁都无法做到。但是如果在判断条件中动一点点手脚,就能够实现上面提到的功能。看一下改造后的代码:
运行后控制台打印了:
惊不惊喜、意不意外?其实它能够执行的秘密就在if的判断条件中。
当第一次调用judge()方法时,不满不或运算中的第一个条件,因此执行第二个条件,会执行匿名内部类内的实例化初始块代码,再次执行judge()方法,此时满足if条件,因此执行第一句打印语句。
而实例化的新对象不满足后面的equals()方法中的条件,所以不满足if中的任意一个条件,因此会执行else中的语句,执行第二句打印语句。
这样就实现了表面上调用一次方法,同时执行if和else中的语句块的功能。怎么样,用这种方式把一段整体的逻辑拆成两块,让你的同事迷惑去吧。
在程序员的世界里,不同语言之间一直存在鄙视链,例如写c的就看不起写java的,因为直接操作内存啥的看上去就很高大上不是么?那么我们今天就假装自己是一个c语言程序员,来在java中操作一把内存。
具体要怎么做呢,还是要使用java中的魔法类Unsafe。看这个名字也可以明白,这玩意如果使用不当的话不是非常安全,所以获取Unsafe实例也比较麻烦,需要通过反射获取:
在拿到这个对象后,我们就可以对内存为所欲为了。例如,我们在实现int a=1;这样的简单赋值时,就可以搞复杂点,像下面这样绕一个弯子:
首先通过allocateMemory方法申请4字节的内存空间后,然后通过putInt方法写入一个1,再从这个地址读取一个int类型长度的变量,最终实现了把1赋值给a的操作。
当然了,还有很多高级一点的用法,这里简单举两个例子。
上面的代码中,通过setMemory方法向每个字节写入byte类型的1,最后调用getInt方法一次性读取4个字节作为一个int型变量的值。这段代码最终打印结果为16843009,对应的二进制如下:
至于c语言中的内存复制,用Unsafe搞起来也是信手拈来:
上面的代码中,通过reallocateMemory方法重新分配了一块8字节长度的内存空间,并把addr开头的4字节内存空间分两次进复制到addr2的内存空间中,上面的代码会打印:
这是因为新的8字节内存空间addr2中存储的二进制数字是下面这样,转化为十进制的long类型后正好对应4294967297。
Unsafe除了能直接操作内存空间外,还有线程调度、对象操作、CAS操作等实用的功能,如果想详细的了解一下,可以看看这篇Java双刃剑之Unsafe类详解,开启新世界的大门。
好了,没用的知识介绍环节就此结束,相信大家在掌握了这些技巧后,都能自带代码混淆光环,写出不一样的拉轰代码。
最后建议大家,在项目中这样写代码的时候,搭配红花油、跌打损伤酒一起使用,可能效果更佳。
那么,这次的分享就到这里,我是Hydra,下篇文章再见。
作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。欢迎添加好友,进一步交流。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。