基于java8
高效简洁编码
- 避免未使用变量的定义,减小变量作用域
- 避免无效对象的创建
- 避免频繁创建大对象,大对象容易进入老年代,频繁创建容易导致老年代频繁GC(例如数据库连接)(默认全部先进入新生代)
- 线程中执行定时任务,需要try-catch,否则一旦出现异常,则定时任务不可用
最小化变量作用域
带有返回值的方法,不应该在方法体内对成员变量修改
利用链式编程
优化前代码
StringBuilder builder = new StringBuilder(96);
builder.append("select id, name from ");
builder.append(T_USER);
builder.append(" where id = ");
builder.append(userId);
builder.append(";");
优化后代码
StringBuilder builder = new StringBuilder(96);
builder.append("select id, name from ")
.append(T_USER)
.append(" where id = ")
.append(userId)
.append(";");
避免循环中使用 " + " 拼接字符串
循环体内,字符串的连接,使用StringBuilder的append方法进行扩展.
反编译出的字节码显示每次循环都会new出一个StringBuilder对象,然后进行append操作,
最后通过toString()方法返回String对象,造成资源浪费.
优化前代码
String str = " ";
for (int i = 0; i < 100; i++) {
str = str + i + " ";
}
优化后代码
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 100; i++) {
builder.append(i).append(" ");
}
如果返回值是boolean类型,没必要使用true或false验证
优化前代码
if (StringUtils.isEmpty(username) == true) {
return "用户名不能为空";
}
if(hasRegistered(username) == false){
//执行注册逻辑
}
优化后的代码
if (StringUtils.isEmpty(username)) {
return "用户名不能为空";
}
//用 ! 替换 ==false 判断
if(!hasRegistered(username)){
//执行注册逻辑
}
if-else-if 中含有return时,可以不使用else
去除else,可以使代码更加简洁
优化前的代码
public int getIndex(double duration) {
if (duration < 20) {
// index [0,20) duration [0,20)
return (int) duration;
} else if (duration < 200) {
// index [20,56) duration [20,200)
return (int) (16 + duration / 5);
} else if (duration < 2000) {
// index [56,92) duration [200,2000)
return (int) (52 + duration / 50);
} else {
return 127;
}
}
优化后的代码
public int getIndex(double duration) {
if (duration < 20) {
// index [0,20) duration [0,20)
return (int) duration;
}
if (duration < 200) {
// index [20,56) duration [20,200)
return (int) (16 + duration / 5);
}
if (duration < 2000) {
// index [56,92) duration [200,2000)
return (int) (52 + duration / 50);
}
return 127;
}
减少使用静态导入
- 静态引入容易造成代码阅读困难,所以在实际项目中应该警慎使用
- 通过
类.静态属性
的方式便于利用开发工具的提示,提高编码效率
泛型类中应该指定泛型类型
便于阅读代码,理解其含有的数据模型
//没有指定泛型
ArrayList users = new ArrayList();
//创建时,不需要指定UserInfo,变量会做类型推断
ArrayList<UserInfo> users = new ArrayList<UserInfo>();
//最佳编码实践
ArrayList<UserInfo> users = new ArrayList<>();
避免通过Map作为方法参数
因为调用方法的开发人员很难知道需要给Map什么样的Key-value
以下是反例:
Cat对外的接口方法要求方法返回值是Map<String, String>,并且key-value都是String类型,
但是内部尽然做Double类型转换,一旦给的value值是不可转换的便会抛出异常.
抛出异常后,将异常信息上传到Cat服务器,开发工具控制台不打印日志,开发过程中很难知道有该异常的存在.
public interface StatusExtension {
public String getId();
public String getDescription();
public Map<String, String> getProperties();
}
com.dianping.cat.status.StatusUpdateTask
private void buildExtensionData(StatusInfo status) {
StatusExtensionRegister res = StatusExtensionRegister.getInstance();
List<StatusExtension> extensions = res.getStatusExtension();
for (StatusExtension extension : extensions) {
String id = extension.getId();
String des = extension.getDescription();
Map<String, String> propertis = extension.getProperties();
Extension item = status.findOrCreateExtension(id).setDescription(des);
for (Entry<String, String> entry : propertis.entrySet()) {
try {
//内部竟然竟然通过Double做类型转换,如果接口返回值不能转成Double,则抛出异常
double value = Double.parseDouble(entry.getValue());
item.findOrCreateExtensionDetail(entry.getKey()).setValue(value);
} catch (Exception e) {
//抛出异常后,开发工具控制台不打印日志,而是上传到Cat服务器
Cat.logError("StatusExtension can only be double type", e);
}
}
}
}
已知即将创建的ArrayList长度时,则在构造方法中指定
ArrayList
默认构造设置的数组长度为 0- 初次调用
ArrayList#add(item)
方法,初始数组长度为 10 - 每次扩容后的长度 = 原来的长度 + 原来长度的一半
(int newCapacity = oldCapacity + (oldCapacity >> 1);)
- 每次扩容都对应新数组的创建,与新旧数组之间数据的拷贝。
已知即将创建的ArrayList长度时,则在构造方法中指定数组长度
如果长度小于10,则可以避免创建多余的数组,
如果长度大于10,则可以避免多次扩容与数组拷贝
优化前代码
ArrayList<UserInfo> olds = new ArrayList<>();
olds.add(new UserInfo("小张", 15000));
olds.add(new UserInfo("小王", 16000));
olds.add(new UserInfo("小李", 8000));
ArrayList<UserInfo> news = new ArrayList<>();
for (UserInfo old : olds) {
old.setSalary(old.getSalary() + 2000);
news.add(old);
}
优化后代码
//创建ArrayList时指定size
ArrayList<UserInfo> news = new ArrayList<>(olds.size());
for (UserInfo old : olds) {
old.setSalary(old.getSalary() + 2000);
news.add(old);
}
考虑if中条件计算的性能
1 例如每一次RPC调用都会经过, invoke方法
2 标签路由功能,一般是在测试时使用, 生产上出现带有标签的服务比较少
3 RpcContext.getContext()会从ThreadLocal拿取RpcContext,如果没有则创建
4 context.getAttachments()返回的是一个HashMap,判断是否存在某个key时,需要计算HashCode,如果出现Hash冲突,还有通过equals判断
但此时如果DubboProperties.consumerTag是个null或者空字符串,上面的逻辑就变成了多余的
优化前的代码
@Override
public Result invoke(Invocation invocation) throws RpcException {
RpcContext context = RpcContext.getContext();
if (!context.getAttachments().containsKey(Constants.TAG_KEY)) {
if (StringUtils.hasText(DubboProperties.consumerTag)) {
context.setAttachment(Constants.TAG_KEY, DubboProperties.consumerTag);
}
}
}
优化后的代码
@Override
public Result invoke(Invocation invocation) throws RpcException {
if (StringUtils.hasText(DubboProperties.consumerTag)) {
RpcContext context = RpcContext.getContext();
if (!context.getAttachments().containsKey(Constants.TAG_KEY)) {
context.setAttachment(Constants.TAG_KEY, DubboProperties.consumerTag);
}
}
........
}
考虑if判断匹配的优先级
假如一个类中会有多个@HColumn,但是只有一个@HRowKey和一个@HVersion,那么匹配到@HColumn的概率更大,所以@HColumn放在if第一个
ReflectionUtils.doWithFields(tableClazz, field -> {
//HColumn 优先判断
if (field.getAnnotation(HColumn.class) != null) {
......
} else if (field.getAnnotation(HRowKey.class) != null) {
......
} else if (field.getAnnotation(HVersion.class) != null) {
......
}
});
不要使用Lombok的@Data注解
生成getter/setter方法之外,会重写equals()
hashCode()
toString()
方法,一旦实体类中有字段变更,方法中的逻辑就会变更,但实际上对业务没什么意义,一般会根据需求有意义的去重写equals()
hashCode()
方法.
推荐使用 @Getter
/@Setter
,生成的代码最简洁。
继承java.io.Serializable接口的类要指定serialVersionUID
private static final long serialVersionUID = -1318390578443889917L;
慎用java8 parallelStream()并行流
Java8中的并行流内部使用的ForkJoinPool
线程池,默认线程数量等于逻辑处理器数量,如果存在耗时任务可能导致其他使用该线程池的任务
一直处于等待状态。
private Map<String, String> getConsumerStatus(Map<String, ConsumerProxy> consumers) {
Map<String, String> consumerStatusMap = Maps.newHashMap();
consumers.entrySet().parallelStream()
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> CompletableFuture.supplyAsync(() -> determineConsumerClient(entry.getValue()))))
.forEach((k, v) -> {
try {
consumerStatusMap.put(getConsumerName(k),
v.get(1_000L * 10, TimeUnit.MILLISECONDS) ? Health.STATUS_DOWN : Health.STATUS_UP);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
LOGGER.error("消费者已关闭:{}", k);
consumerStatusMap.put(k, Health.STATUS_DOWN);
}
});
return consumerStatusMap;
}
- 该代码中在调用使用了
parallelStream()
来开启并行流,而在Collectors
收集器时使用了CompletableFuture
来异步执行并返回CompletableFuture,
在forEach中等待任务完成。已经开启异步任务,这里使用并行流没有意义,反而会照成线程上下文的切换,浪费CPU资源。 - forEach时其实已经回到主线程工作。
- 该代码中Map是通过Guava的Maps.newHashMap()创建,其内部就是new HashMap<>(),建议直接使用,避免依赖Guava。
关于Java8 stream的其他注意点
forEach(Consumer)
遍历不需要调用stream()
,避免创建多于的Stream对象
优化前代码
strings.stream().forEach(System.out::println);
优化后代码
默认串行打印,有序,内部直接实际上是直接遍历ArrayList中的元素
strings.forEach(System.out::println);
不推荐流化之后遍历
hashMap.entrySet().stream().forEach((entry)->{
System.out.println(entry.getKey());
System.out.println(entry.getValue());
});
优化后代码
内部是直接遍历Map的节点数据,无需流化过程
hashMap.forEach((key, value) -> {
System.out.println(key);
System.out.println(value);
});
并行流,打印的数据无序
strings.parallelStream().forEach(System.out::println);
并行流处理后,通过collect收集元素放到List中,forEach(Consumer)遍历时,已经回到主线程
strings.parallelStream().filter(Main3::test).map(item -> item + "...")
.collect(Collectors.toList())
.forEach(System.out::println);
//等效于如下代码段
List<String> list = strings.parallelStream().filter(Main3::test).map(item -> item + "...")
.collect(Collectors.toList());
list.forEach(System.out::println);
使用List#addAll(…)或者Map#putAll(…)将一个集合或数组中的元素添加到另一个集合中
- 每次调用List#add(item)方法都会对内部的数组长度进行判断,一旦ArrayList数组长度不够,则会扩容原来长度的一半。
- 对于HashMap每次扩容为原来的2倍,数组长度达到64之后,变成红黑树。
- 而扩容则导致原来的数据全部需要重新复制到新数组,频繁的扩容,会越低性能。
优化前代码
private static ArrayList<Field> getFields(Class<?> clazz) {
ArrayList<Field> fields = new ArrayList<>();
Class<?> searchType = clazz;
while (Object.class != searchType && searchType != null) {
Field[] declaredFields = getDeclaredFields(searchType);
for (Field declaredField : declaredFields) {
fields.add(declaredField);
}
searchType = searchType.getSuperclass();
}
return fields;
}
优化后代码
推荐方式,通过Arrays.asList(declaredFields)
将数组包装成List,通过addAll()
,
可以一次性添加在新集合中,避免多次扩容拷贝
private static ArrayList<Field> getFields(Class<?> clazz) {
ArrayList<Field> fields = new ArrayList<>();
Class<?> searchType = clazz;
while (Object.class != searchType && searchType != null) {
Field[] declaredFields = getDeclaredFields(searchType);
fields.addAll(Arrays.asList(declaredFields));
searchType = searchType.getSuperclass();
}
return fields;
}
慎用正则
Java中通过Pattern.compile(regex)
编译正则表达式,但是这是一个过程比较耗时,官方文档中指明不要重复编译相同的正则,
该方法返回的Pattern对象是一个不可变对象,可以在多线程之间共享使用。而其matcher()
方法返回的Matcher
对象不是线程安全的。
如果一个正则可能被多次大量的使用, 可以定义其全局变量重复使用.
注意String的replaceAll方法内部每次都会编译正则, String提供的很多方法内部会使用正则,使用时应该注意性能问题.
优化前
for (String propertyName : propertyNames) {
List<String> indexs = extractIndex(propertyName);
if (indexs.size() > 0) {
//String的replaceAll方法内部每次都会编译正则,效率低
String oldKeyTemplate = propertyName.replaceAll("\\[([^]]+)", "[%s");
String newkeyTemplate = oldNewMapping.get(oldKeyTemplate);
if (newkeyTemplate != null) {
String prefix = oldKeyTemplate.substring(0, oldKeyTemplate.indexOf("[") + 1);
EnumerablePropertySource<?> firstArraySource = firstArraySourceAdapterRecord.computeIfAbsent(prefix, (key) -> source);
if (firstArraySource.getName().equals(source.getName())) {
String newkey = String.format(newkeyTemplate, indexs.toArray());
//Array property adaptation
adapterSource.putIfAbsent(newkey, "${" + propertyName + "}");
}
}
}
}
优化后
将编译后的正则Pattern对象定义成for循环外的变量,在循环中重复使用.(可以是for循环外的局部变量,成员变量,全局静态变量),视情况而定。
private static Pattern ARRAY_PATTERN = Pattern.compile("\\[([^]]+)");
for (String propertyName : propertyNames) {
List<String> indexs = extractIndex(propertyName);
if (indexs.size() > 0) {
String oldKeyTemplate = ARRAY_PATTERN.matcher(propertyName).replaceAll("[%s");
String newkeyTemplate = oldNewMapping.get(oldKeyTemplate);
if (newkeyTemplate != null) {
......
}
}
}
使用Map的computeIfAbsent方法
map.computeIfAbsent(key, mappingFunction)
第一个参数为key,第二个参数为函数式接口,
如果Map中含有指定的key,则返回key对应的Value,
如果Map中不含有指定的key,则调用函数式接口中的表达式创建对象,并且返回新创建的对象。
优化前
private Node getCurNode(String time) {
Node curTimeNode = nodes.get(time);
if (curTimeNode == null) {
curTimeNode = new Node();
nodes.put(time, curTimeNode);
}
return curTimeNode;
}
优化后
private Node getCurNode(String time) {
return nodes.computeIfAbsent(time, (key) -> new Node());//不存在时才会创建Node对象
}
Map的computeIfAbsent方法和putIfAbsent区别
对于putIfAbsent方法
如果Map中含有指定的key,则返回key对应的Value,
如果Map中不含有指定的key,则将value添加到map中,但返回值是null。
UserInfo aaa2 = map.putIfAbsent("aaa", new UserInfo());
System.out.println(aaa); //print null
//即使存在aaa这个key,也会创建一个新的无意义的UserInfo对象,返回的旧的UserInfo对象
UserInfo aaa2 = map.putIfAbsent("aaa", new UserInfo());
UserInfo aaa3 = map.putIfAbsent("aaa", new UserInfo());
System.out.println(aaa2 == aaa3); //print true
java8 方法引用注意点
一个方法引用对应会在运行时动态的创建一个Class,并实例化一个对象.
//动态的创建一个 类
@Test
public void test(){
SerializableFunction<UserInfo, List<Address>> fun = UserInfo::getAddresses;
ReflectionUtil.getFieldName(fun);
}
//两次调用同一个方法的方法引用,会动态的创建两个不同的 类
@Test
public void test(){
ReflectionUtil.getFieldName(UserInfo::getAddresses);
ReflectionUtil.getFieldName(UserInfo::getAddresses);
}
Java8 方法引用动态创建出的class是什么样子?
通过配置如下属性可以保存动态生成的class
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
方法引用动态创建出的class
final class CommonTest$$Lambda$1 implements Function {
private CommonTest$$Lambda$1() {
}
@Hidden
public Object apply(Object var1) {
return ((UserInfo)var1).getAddresses();
}
}
这个会创建几个 类 ??? 答案: 一个
for (int i = 0; i < 10; i++) {
ReflectionUtil.getFieldName(UserInfo::getAddresses);
}
使用java7的方法句柄替代反射
使用java7的 try-with-resource 语句时,注意释放的对象需要实现java.io.Closeable接口
SimpleDateFormat 不是线程安全的
-
SimpleDateFormat#format(date)
不是线程安全,多线程情况下不同的Date,可能格式化出相同的字符串,
但是不抛出异常(非常难以排查的问题) -
SimpleDateFormat#parse("2020-05-10 20:42:15");
不是线程安全,多线程情况下会抛出如下异常java.lang.NumberFormatException: For input string: "" java.lang.NumberFormatException: multiple points java.text.ParseException: Unparseable date: "2020-05-10 20:42:15"
为什么jackson里中只传递了一个SimpleDateFormat对象却可以在多线程情况下使用 ???
示例代码如下:
//objectMapper可以在多线程情况下使用
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm"));
注意: new SimpleDateFormat()本身是一个比较耗时的过程。
原因在于SimpleDateFormat
实现了java.lang.Cloneable
接口,jackson在序列化日期时, 会通过其clone()
方法创建一个
新的SimpleDateFormat对象, 对影响线程安全的成员属性使用了深拷贝
, 其他属性则使用浅拷贝。
所以其运行性能要比每次都通过new关键字创建一个新的对象还要快.
推荐使用 Java8 的时间API
/** Created by bruce on 2020-05-10. */
public class DateUtils {
private static ZoneId zone = ZoneId.systemDefault();
private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/** Date 转 时间字符串,格式: yyyy-MM-dd HH:mm:ss */
public static String toString(Date date) {
if (date == null) {
return null;
}
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), zone);
return dtf.format(localDateTime);
}
/** Date 转 时间字符串,格式: yyyy-MM-dd HH:mm:ss */
public static String toString(LocalDateTime localDateTime) {
if (date == null) {
return null;
}
return dtf.format(localDateTime);
}
//字符串时间yyyy-MM-dd HH:mm:ss 转 LocalDateTime
public static LocalDateTime parseDate(String time) {
return LocalDateTime.parse(time, dtf);
}
/** 字符串时间yyyy-MM-dd HH:mm:ss 转 Date */
public static Date toDate(String time) {
return Date.from(Instant.from(LocalDateTime.parse(time, dtf).atZone(zone)));
}
}
DecimalFormat不是线程安全的,那么format方法被多线程调用是否安全?
//这段代码在多线程环境下正常运行
private DecimalFormat df = new DecimalFormat("0.00");
public String format() {
df.format(value)
}
- 对一些共享变量只是读操作,不会产生线程安全问题
- 调用format时,会对修改的成员变量(
DigitList digitList
)加锁
DecimalFormat不是线程安全的,使用DecimalFormat#parse(s)会抛出异常
如下示例在多线程情况下这里会抛出异常
@Test
public void test5() throws InterruptedException {
DecimalFormat df = new DecimalFormat("0.00");
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
String v = ThreadLocalRandom.current().nextDouble(100000) + "";
try {
countDownLatch.await();
Number parse = df.parse(v);
System.out.println(v + " " + v + " Number:" + parse);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);
countDownLatch.countDown();
Thread.sleep(5000);
System.out.println("运行结束");
}
如何解决DecimalFormat线程安全问题?
1、每次new一个新的DecimalFormat,但是这种方式性能比较低
DecimalFormat df = new DecimalFormat("###.##");
df.parse("125.178");
2、通过ThreadLocal为每一个线程绑定一个DecimalFormat对象
static final ThreadLocal<DecimalFormat> DECIMAL_FORMATTER = ThreadLocal.withInitial(() -> {
return new DecimalFormat("0.##");
});
DECIMAL_FORMATTER.get().format(v);
3、通过DecimalFormat的clone()方法,复制一个新的对象.(推荐方式)
((DecimalFormat) df.clone()).parse("125.178");