Java高效编程:总结分享
参考资料:慕课网:Java高效编程收费实战课程、博客园、CSDN、菜鸟教程以及其他文档。
篇幅受限,不太想针对每个点都写篇博客,有的地方可能写的不是很详细,一笔带过了。如果你觉得那个点在项目中用得上可以另行搜索一些相关的更详细的博客或文档。
1.Lambda和函数式编程
函数式编程即可以把函数当作变量、参数、返回值传递。实现的方法就是定义一个函数式接口,函数式接口即只有一个抽象方法的接口。这个接口类型的变量就可以当作参数传递,而在传递的时候可以通过匿名内部类把要执行的逻辑代码传递进去。
函数式接口如下,其目的是操作两个数,具体的逻辑代码在使用时通过匿名内部类实现
interface Handle{ int operation(int a, int b); }
使用:假设我们要调用一个use方法,其接收参数为两个int和一个函数接口Handle类型的参数,然后要通过传递过来的逻辑代码处理这两个int
use(1,2,new Handle(){ int operation(int a,int b){ return a+b; } }));
而在use方法里面,就已经接收到了1、2和匿名内部类里面的逻辑代码。
public void use(int a,int b,Handle h){ System.out.println(h.operation(a,b));//输出3 }
此时Lambda的作用就是简化匿名内部类的写法。如果替换成Lambda表达式,上面调用use方法的代码就变成了这样:
use(1,2,(a,b)->a+b);
下面说说Lambda的语法:
(参数)->{代码块}
1. 如果只有一个参数可以不要小括号。
2. 如果代码块只有一句可以不要大括号,就像我上面写的a+b一样。
3. 需要返回值的话,如果只有一句代码可以不用写return。就像我上面写的,a+b,只有一句,需要一个int类型的返回值,这时候默认就返回a+b的结果。
4. 可选的类型声明,在小括号中,既可以写上int,也可以不写,它都会根据参数识别类型。
5. Lambd表达式的目标类型必须是函数式接口,就如上面代码的Handle类型。Lambda表达式也是有类型的,不然也不能当作参数传递。所以如果为了代码清晰,你不想在传递参数时写Lambda表达式,也可以这么写:
Handle h=(a,b)->a+b
然后把h传递给use方法。
函数式接口一般来说都比较泛化,Java专门给我们提供了一些函数式接口,所以不一定需要我们自己定义。比如:
2.使用方法引用精简Lambda
方法引用是对Lambda的简化,如果Lambda表达式代码块中只有一条代码,就可以使用方法引用,其语法如下:
类名或对象::方法名
下面举例的每种情况,前面为Lambda代码,后面为简化引用后的代码
1.静态方法的引用:
String num->Integer.parseInt(num); Integer::parseInt;
2.任意类型的实例方法的方法引用:
String str->str.length(); String::length;
3.引用指定对象的实例方法:
List<String> list=new ArrayList<>();//list为指定对象 String str->list.add(str); list::add;
3.使用Stream操作集合
首先了解什么是Stream?Stream类似SQL语句一样,对数据源进行各种操作,如筛选,排序等,而Stream本身不会存储数据。简而言之就是:Stream是从支持数据处理操作的数据源生成的元素队列。数据源可以是集合数组等。数据处理操作就是各种筛选、排序等,下面会重点讲。
集合在实际代码中会经常用到,而Stream可以提供更简便的数据操作,其很多API接收的还是函数式接口,所以还可以用上前面的Lambda表达式,进一步简化代码。
Stream提供的API从功能上主要分为四类:中间方法无状态、中间方法有状态、末端短路方法和末端非短路方法。
所谓中间和末端:中间方法就是保持流的打开状态,并允许后续还有操作,比如排序、遍历等。末端方法就是对流的最终操作,比如最大(小)值、遍历、计数等。
有无状态和是否短路:有状态就是这种方法会改变流的结果,如去重、截断等,这种方法性能开销会更大。无状态相反。短路方法就是这种方法不一定会检查所有元素,比如操作首个值,不匹配等,找到了就返回。非短路相反。
中间方法无状态:过滤(filter)、映射(map)、扁平化(flatMap)、遍历(peek)
中间方法有状态:去重(distinct)、跳过(skip)、截断(limit)、排序(sorted)
末端短路方法:所有匹配(allMatch)、任意匹配(anyMatch)、不匹配(noneMatch)、查找首个(findFirst)、查找任意(findAny)
末端非短路方法:遍历(forEach)、归纳(reduce)、最大值(max)、聚合(collect)、最小值(min)、计数(count)
下面演示几个示例,假设有个Person实体类,里面有字段gender、name、age,list为Person类型的集合。stream()方法作用是为集合创建串行流。
list.stream().filter(person -> person.getAge()>20&&person.getGender().equals("男")) .forEach(person -> System.out.println("姓名:"+person.getName()));//输出所有大于20岁男性的姓名
如果筛选过程你在数据库里就已经做了,现在只想返回结果中的某个字段,可以这么做。
list.stream().map(person -> person.getName())//map是把一种类型的集合元素转化成另一种类型的集合元素。如Person集合转化成String集合,其内容就是name .forEach(name-> System.out.println(name));//输出所有name
对年龄去重并排序
list.stream().map(person -> person.getAge())
.distinct().sorted()
.forEach(age-> System.out.println(age));
上面几个示例只演示了一个末端方法,更多的API大家可以自己去试试。实际中我们筛选出数据后一般不会直接输出,而是返回给客户端,这时候就需要把筛选过后的流给收集起来:
List<Person> list1 = list.stream().filter(person -> person.getAge() > 18) .collect(Collectors.toList());//收集到一个集合中 Map<String,List<Person>> list2 = list.stream() .collect(Collectors.groupingBy(person->person.getGender()));//按性别分类
4.try-with-resource
这个是JDK7中提供的一个语法糖。可以在try后面的小括号里面声明资源,之后就不用再在finally里手动的关闭了。但也不是所有资源都可以自动关闭,必须实现了AutoCloseable接口。使用场景:打开外部资源时可以用try-with-resource自动关闭,如读取文件,数据库连接,网络连接等。在声明的时候就可以在try后面的小括号里声明,不过最好注意一下,这些类是否实现了上面提到的接口,还有其源码中是否有关闭方法。比如ByteArrayInputStream等,它的close方法里面什么也没有,说明不需要关闭,所以就不需要声明在try后的小括号里了。
5.使用Optional避免空指针异常
空指针异常应该是很多程序员见过最多的异常。如果想避免这个异常,经常会使用if判断是否为null,这样显得一点都不精简。Optional是一个可以为null的容器对象。
还是以上面的Person实体类为例,如果我们想安全的返回某个Person类型数据的name,正常的话应该这么写:
Optional的API就不在这写了,在看下面的内容之前,建议先看看它的API:https://www.runoob.com/java/java8-optional-class.html.根据API说明,自己多试试。
public static String Get_Name(Person person) throws Exception{ if(person!=null){ if(person.getName()!=null){ return person.getName(); } } throw new Exception("Person为空"); }
使用Optional后可以这样
public static String Optional_Get_Name(Person person) throws Exception{ return Optional.ofNullable(person).map(name->person.getName()).orElseThrow(()->new Exception("Person为空")); }
实际情况中更多的可能是这样:
if(person!=null){ dosomthing(person);//对person对象执行某种逻辑代码 }
使用Optional后可以这样
Optional.ofNullable(person).ifPresent(p->{dosomthing(p);});
总结:
1.可以发现ofNullable在每种情况下使用了,它的作用就是构建一个Optional对象。如果传进去的值为空而且后面没有任何操作它就返回empty对象。
2.ofNullable和of的区别就是,如果值为null,of会报空指针异常。如果你想收集这个异常,那么就可以用of,一般情况下就用ofNullable。
3.orElse开头的三个方法,是针对空值进行操作的;两个map方法是做映射的,就像Stream中的map一样;两个Present方法是判断值是否为空的,is开头的直接返回 boolean类型,if开头的可以用Lambda表达式对非空的值做一些操作。
6.Guava工具集
Guava是谷歌开发的一款拓展Java类库的工具集。像上面说到的Optional就是Java开发团队受到这个工具集中Optional的启发。我的个人项目中就用到了Guava提供的RateLimiter限流、Multiset、还有一些工具类。如果你用的是Maven,在https://mvnrepository.com/artifact/com.google.guava/guava导入依赖就行,如果不是可自行在网上搜索jar包导入。
新集合Multiset:
Multiset是一个可以有重复值的Set集合。其特点是无序可重复。具体的API不在这里讲了,大家可以看看这个http://www.bjpowernode.com/tutorial_guava/731.html。其可实现这么一个比较常见的场景,在一个集合中,统计出某个元素的出现的次数,利用这个集合可以很轻松的实现这个功能。还是以Person实体类为例,如果大家用的也是实体类,不要忘了重写hashCode()和equals()方法:
List<Person> list=dao.Get_Personlist();//dao.Get_Personlist()模拟从数据源获取perosn集合 Multiset<Person> multiset= HashMultiset.create();//HashMultiset为实现类 list.stream().forEach(person -> multiset.add(person));//通过Stream和Lambda依次把元素加入multiset int count=multiset.count(StartPerson)//StartPerson为假设的指定的Person对象,count就是找出StartPerson出现的次数
原生集合的拓展工具类:
除了Guava提供的一些新的集合,还针对Java原生集合提供了很多工具类,命名上基本都以原生集合名加s,比如List集合的工具类是Lists。工具类主要就是看API,由于不可抗拒的原因,谷歌官方的API是看不了了,可以看这个https://ifeve.com/google-guava-collectionutilities/
7.Lombok注解
Lombok也是一个拓展类库,提供了一些注解为我们省略了很多重复但经常会用到的代码,比如实体类里面的get,set,equals等。下图是其常用的注解:
其中@Data注解是包括@Getter等四个注解的功能。和Guava一样,在Maven里导入依赖https://mvnrepository.com/artifact/org.projectlombok/lombok,或自行下载jar包。这里还是推荐大家使用Maven,平时下载jar包也方便些,自己在网上下载的一个lombok jar包导入后,没有生效,换成Maven后就可以了。与普通的依赖不同,这里多了个 <scope>provided</scope>,它的意思就是这个jar包是运行在编译时期,加上注解后,并不会多出任何代码,javac编译成class文件后,大家用IDEA打开这个class文件就会发现多了很多代码。
还有一点要注意的是,这个需要下载插件。Lombok注解是在编译时才添加代码的,当我们编写代码时调用get、set方法,实际上这时候这些方法代码并不存在,IDE语法检测就不会通过,如果用的是IDEA在settings->plugins里搜索Lombok下载安装就行了。
简单的示例:
@Data public class EntityDemo { private String name; private int age; }
编译后查看class文件,用IDEA反编译:
public class EntityDemo { private String name; private int age; public EntityDemo() { } public String getName() { return this.name; } public int getAge() { return this.age; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public boolean equals(Object o) { if (o == this) { return true; } else if (!(o instanceof EntityDemo)) { return false; } else { EntityDemo other = (EntityDemo)o; if (!other.canEqual(this)) { return false; } else { Object this$name = this.getName(); Object other$name = other.getName(); if (this$name == null) { if (other$name == null) { return this.getAge() == other.getAge(); } } else if (this$name.equals(other$name)) { return this.getAge() == other.getAge(); } return false; } } } protected boolean canEqual(Object other) { return other instanceof EntityDemo; } public int hashCode() { int PRIME = true; int result = 1; Object $name = this.getName(); int result = result * 59 + ($name == null ? 43 : $name.hashCode()); result = result * 59 + this.getAge(); return result; } public String toString() { return "EntityDemo(name=" + this.getName() + ", age=" + this.getAge() + ")"; } }
其中@Getter和@Setter可以用在字段前面,给字段单独使用,还可以在后面小括号里设置一些属性
public class EntityDemo { @Getter(value = AccessLevel.PRIVATE,//设置get方法访问权限为private onMethod_= {@NonNull}//再给name加个NotNull注解 ) private String name; private int age; }
这些注解的功能是非常实用的,用起来也比较简单,也就不一一举例了,平时比较常用也就是@Data相关的和构造方法还有就是日志注解。主要就是它们的一些属性可能会比较陌生,大家可以Ctrl加鼠标左键点进源码,看看它们的字段,也就是它们可以设置的属性,通过字面意思也应该能知道它们的意思。不过除了日志注解,实际当中我基本没用其他注解,一个是降低了代码的可读性,二是错误异常可能正好在这些生成的代码中,增加了调试难度。IDEA的Ctrl+O和Alt+Insert快捷键就足够了
8.验证框架和约束注解
Bean Validation和Hibernate Validator
前者是验证框架的一种规范,后者是对其的一种具体实现。使用时先导入这两个依赖:https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator 和https://mvnrepository.com/artifact/javax.validation/validation-api。约束注解对字段属性设置约束或规则,验证框架则是对实际数据进行验证,验证是否符合对应的约束或规则。
下面列出一些常用的约束注解,每个注解基本都有一些属性可以设置,和上面说的一样,大家可以点进注解源码查看有哪些字段:
注解 |
描述 |
@AssertFalse |
被注释的元素必须为 false |
@AssertTrue |
同@AssertFalse |
@DecimalMax |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin |
同DecimalMax |
@Digits |
被注释的元素是数字 |
@Future |
将来的日期 |
@Max |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Min |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@NotNull |
不能是Null |
@Null |
元素是Null |
@Past |
被注释的元素必须是一个过去的日期 |
@Pattern |
被注释的元素必须符合指定的正则表达式 |
@Szie |
被注释的元素必须在指定长度内 |
|
元素必须是格式良好的电子邮箱地址 |
@Length |
字符串的大小必须在指定的范围内,有min和max参数 |
@NotEmpty |
字符串的不能是空 |
@NotBlank |
字符串不能使空,但是与@NotEmpty不同的是尾随的空白被忽略 |
@URL |
字符串必须是一个URL |
简单的使用,还是以Person实体类为例(大家在尝试时最好在web项目上,毕竟这个功能基本也都在web上才用得到。非web项目可能会出现一些错误,这时候就要引入el依赖):
@Data @AllArgsConstructor public class Person { @NotNull(message = "用户名不能为空") @Size(min = 2,max = 15) private String name; @NotNull private int age; @NotNull(message = "性别不能为空") private String gender; }
private static Validator validator=Validation.buildDefaultValidatorFactory().getValidator();//初始化验证器 public static void main(String[] args) { Person person=new Person("",15,"男");//新建一个数据 Set<ConstraintViolation<Person>> validate = validator.validate(person);//进行验证 validate.forEach(item ->System.out.println(item.getMessage()));//对验证结果进行输出,就是输出注解后面的message。 }
9.IDEA使用技巧
IDEA应该是目前Java开发者用的比较多的IDE了,在这分享一些小技巧。全部的快捷键就不重复说了,网上有很多说明。
1.右键行号上的红色断点图标可以给断点添加判断条件,符合条件才会在这个断点停下。
2.Debug运行后按Alt+F8(Windows如果是N卡可能会快捷键冲突),或点击Debug栏上类似一个计算器的图标(在“跳转到光标”按钮的右边)。会打开一个小窗口,里面可以对断点处的数据进行计算,比如一个断点处有一个list,我们可以输入list.size()计算出它的元素个数。
3.Stream调试,当我们在Stream代码处打上断点,运行后Debug栏上会多出一个按钮(在“跳转到光标”按钮的右边第二个),可以看每一步stream操作后的数据集结果。如果没有这个按钮可能是你的IDEA版本太低了,在插件里面搜索Java Stream Debugger安装就行了。
4.全局重命名,首先鼠标光标放到一个变量名上(方法名也行),按住Shift+F6,重命名后,所有用到这个变量的地方都会自动重命名。
5.鼠标光标放到一个方法名上,按Ctrl+F6可以快捷的修改方法签名(或点击顶部工具栏Refactor->ChangeSinature)。如访问修饰符、返回类型、方法名、参数列表。新增一个参数后,还可以设置默认值,调用了这个方法的地方就会自动填上这个默认值。
6.在做后端开发时,经常会用到postman或者其他请求工具测试接口,有时候可能会需要发送json格式的数据,在插件里面搜索POJO to Json插件,安装重启后鼠标右键实体类名,有个POJO to Json的选项,点击后就会自动复制这个实体类所有字段的Json格式,粘贴一下就可以用了。
7.序列化版本ID生成器插件:GenerateSerialVersionUID,插件里搜索UID就行了。在用Redis时、或实体类实现了Serialiable接口。如果改变了实体类结构或者其他问题可能会在查询Redis数据后反序列化时报错,报错内容就是序列化版本id不一致,如果我们给实体类指定一个固定的序列化版本ID就不会出现这个问题了。而这个插件可以帮助我们一键自动生成,快捷键Alt+Insert,选择最后一个SerialVersionUID。
8.Mybatis Log Plugin,平时开发时,即使我们在配置文件里面设置了在控制台输出sql语句,但是还是会以?号占位符的形式显示。这个插件可以显示完整可直接执行的sql语句。
9.注释后写TODO,注释会以黄色高亮的方式显示,如果我们有一些暂时处理不了的问题,就可以使用这个注释提示我们。如//TODO 这里有个bug需要修改