java高效简洁编码

基于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;
}
减少使用静态导入
  1. 静态引入容易造成代码阅读困难,所以在实际项目中应该警慎使用
  2. 通过类.静态属性的方式便于利用开发工具的提示,提高编码效率
泛型类中应该指定泛型类型

便于阅读代码,理解其含有的数据模型

//没有指定泛型
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长度时,则在构造方法中指定
  1. ArrayList默认构造设置的数组长度为 0
  2. 初次调用ArrayList#add(item)方法,初始数组长度为 10
  3. 每次扩容后的长度 = 原来的长度 + 原来长度的一半(int newCapacity = oldCapacity + (oldCapacity >> 1);)
  4. 每次扩容都对应新数组的创建,与新旧数组之间数据的拷贝。

已知即将创建的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;
}
  1. 该代码中在调用使用了parallelStream()来开启并行流,而在Collectors收集器时使用了CompletableFuture来异步执行并返回CompletableFuture,
    在forEach中等待任务完成。已经开启异步任务,这里使用并行流没有意义,反而会照成线程上下文的切换,浪费CPU资源。
  2. forEach时其实已经回到主线程工作。
  3. 该代码中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(…)将一个集合或数组中的元素添加到另一个集合中
  1. 每次调用List#add(item)方法都会对内部的数组长度进行判断,一旦ArrayList数组长度不够,则会扩容原来长度的一半。
  2. 对于HashMap每次扩容为原来的2倍,数组长度达到64之后,变成红黑树。
  3. 而扩容则导致原来的数据全部需要重新复制到新数组,频繁的扩容,会越低性能。

优化前代码

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 不是线程安全的
  1. SimpleDateFormat#format(date) 不是线程安全,多线程情况下不同的Date,可能格式化出相同的字符串,
    但是不抛出异常(非常难以排查的问题)

  2. 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)
}
  1. 对一些共享变量只是读操作,不会产生线程安全问题
  2. 调用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");
ThreadLocal 有一定的内存泄露的风险
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值