Loading...
墨滴

Bayanbulake

2021/12/05  阅读:59  主题:自定义主题1

Java | 编码的艺术

思考

在《调香术》这本书里面有这样一句话,<font color= “#660066" >“人所共知,调香是科学技术和艺术的结合,在调配高级香水、空气清新剂、化妆品香精时。“艺术”二字占有更大的分量,同艺术家、画家一样,调香师的艺术修养决定了他的作品到达的境界。

如果对这句话引深一下,我觉得编程也是如此,<font color= “#660066" >开发人员对编码理解和领悟的水平高低,决定了其编码艺术水平到达的境界,越是优雅、简洁的代码,其艺术水平就越高,更容易让人产生心理共鸣,进而学习和模仿。

在工作和学习中,经常会接触到同事或者网友写的代码,在梳理业务逻辑和模块时,总能发现十分让人心生欢喜的代码,其优雅、简洁的写法和构思,让人心生敬意,不自觉的收藏,以便后续研读和学习。

本着学习无止境的态度,加之本人水平一般,将自己看到和总结的<font color= “#660066" >具有艺术级别的代码罗列如下,与诸君共读。

研读

1. 尽量使用基本数据类型,避免自动装箱和拆箱

正例

int i = 10;

反例

Integer i = 10;

直接赋值避免了对象的新建,变量可以直接指向值的引用,简洁且高效。

2. 如果变量的初值会被覆盖,就没有必要给变量赋初值

正例

List<UserDO> userList;
if (isAll) {
    userList = userDAO.queryAll();
else {
    userList = userDAO.queryActive();
}

反例

List<UserDO> userList = new ArrayList<>();
if (isAll) {
    userList = userDAO.queryAll();
else {
    userList = userDAO.queryActive();
}

userList确定会被赋值,就无须初始化,这样可以少创建引用或者new对象。

3. 尽量使用函数内的基本类型临时变量

正例

public final class Accumulator {
    private double result = 0.0D;
    public void addAll(@NonNull double[] values) {
        double sum = 0.0D;
        for(double value : values) {
            sum += value;
        }
        result += sum;
    }
    ...
}

反例

public final class Accumulator {
    private double result = 0.0D;
    double sum = 0.0D;
    public void addAll(@NonNull double[] values) {
        for(double value : values) {
            sum += value;
        }
        result += sum;
    }
    ...
}

缩小变量的作用域,可以避免频繁的GC,减轻JVM的GC压力。

4. 直接赋值常量值,禁止声明新对象

正例

String s = "mua~";

反例

String s = new String("mua~");

直接赋值常量值,创建了一个对象引用,这个对象引用指向常量值。如果是声明新对象的话,则会在虚拟机内存中new一个对象,占用内存会更大。

5. 如果确定值无需修改,则定义为静态常量

正例

 private static final long TIMEOUT = 5L;

反例

 private  final long TIMEOUT = 5L;

在《Java编程思想》对static的描述为:

“static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。”

其实static 关键字的主要作用是为了节省内存。

6. 变量尽量定义在循环体内

正例

for(int i = 0; i < 10; i++){
    User user = new User();
    //···
}

反例

User user = new User();
for(int i = 0; i < 10; i++){
    //···
}

Java8之后,在循环体内和循环体外定义变量对循环的性能已经没有影响了。为了缩小变量的作用域,我们根据实际情况应该把变量定义在循环体内,也能减轻GC压力。

7. 采用Lambda表达式替换内部匿名类

正例

List<User> userList = ...;
Collections.sort(userList, (user1, user2) -> {
    Long userId1 = user1.getId();
    Long userId2 = user2.getId();
    ...
    return userId1.compareTo(userId2);
});

反例

List<User> userList = ...;
Collections.sort(userList, new Comparator<User>() {
    @Override
    public int compare(User user1, User user2) {
        Long userId1 = user1.getId();
        Long userId2 = user2.getId();
        ...
        return userId1.compareTo(userId2);
    }
});

Lambda表达式在大多数虚拟机中采用invokeDynamic指令实现,相对于匿名内部类在效率上会更高一些。

8. 尽量指定类的final修饰符

正例

public final class DateHelper {
    ...
}

反例

public class DateHelper {
    ...
}

如果指定了一个类为final,则该类所有的方法都是final的,Java编译器会寻找机会内联所有的final方法。内联对于提升Java运行效率作用重大,具体可参见Java运行期优化,能够使性能平均提高50%。

9. 把跟类成员变量无关的方法声明成静态方法

正例

public static int getMonth(Date date) {
  Calendar calendar = Calendar.getInstance();
  calendar.setTime(date);
  return calendar.get(Calendar.MONTH) + 1;
}

反例

public int getMonth(Date date) {
  Calendar calendar = Calendar.getInstance();
  calendar.setTime(date);
  return calendar.get(Calendar.MONTH) + 1;
}

静态方法不再属于某个对象,而是属于它所在的类。只需要通过其类名就可以访问,不需要再消耗资源去反复创建对象。即便在类内部的私有方法,如果没有使用到类成员变量,也应该声明为静态方法。

因此在实际开发中,我们更喜欢把一些公用的方法封装进Utils里面,其实就是对此类思想最好的实践。

10. 协议方法参数值非空,避免不必要的空指针判断

正例

public static boolean isValid(@NonNull UserDO user) {
  return Boolean.TRUE.equals(user.getIsValid());
}

反例

public static boolean isValid(UserDO user) {
    if (Objects.isNull(user)) {
        return false;
    }
  return Boolean.TRUE.equals(user.getIsValid());
}

为了避免繁琐的空指针判断,可以@NonNull和@Nullable标注参数。

11. 尽量减少方法的重复调用

正例

List<UserDO> userList = ...;
int userLength = userList.size();
for (int i = 0; i < userLength; i++) {
    ...
}

反例

List<UserDO> userList = ...;
for (int i = 0; i < userList.size(); i++) {
    ...
}

当userList.size()很大的时候,我们就有必要把其声明为一个变量,这样可以大大减少重复计算的次数。

12. 尽量使用移位来代替正整数乘除

正例

int num1 = a << 2;
int num2 = a >> 2;

反例

int num1 = a * 4;
int num2 = a / 4;

基于计算机二进制计算原理,在进行乘除2^n(n为正整数)的正整数计算时,如果使用位运算效率会高许多。

13. 使用StringBuilder进行字符串拼接

正例

StringBuilder sb = new StringBuilder(128);
for (int i = 0; i < 10; i++) {
    if (i != 0) {
        sb.append(',');
    }
    sb.append(i);
}

反例

String s = "";
for (int i = 0; i < 10; i++) {
    if (i != 0) {
        s += ',';
    }
    s += i;
}

String是final类,内容不可修改,所以每次字符串拼接都会生成一个新对象。StringBuilder在初始化时申请了一块内存,以后的字符串拼接都在这块内存中执行,不会申请新内存和生成新对象。

14. 不要使用""+转化字符串

正例

int i = 12345;
String s = String.valueOf(i);

反例

int i = 12345;
String s = "" + i;

String.valueOf()方法虽然看起来麻烦,但性能很好。

15. 初始化集合时,尽量指定集合大小

正例

Set<Long> userSet = new HashSet<>(100);

反例

Set<Long> userSet = new HashSet<>();

如果不指定初始值的大小,当默认大小不再满足数据需求时就会扩容,每次扩容的时间复杂度有可能是O(n)。所以,尽量指定预知的集合大小,就能避免或减少集合的扩容次数。

16. 用JDK提供的方法拷贝集合

正例

List<UserDO> user1List = ...;
List<UserDO> user2List = ...;
List<UserDO> userList = new ArrayList<>(user1List.size() + user2List.size());
userList.addAll(user1List);
userList.addAll(user2List);

反例

List<UserDO> user1List = ...;
List<UserDO> user2List = ...;
List<UserDO> userList = new ArrayList<>(user1List.size() + user2List.size());
for (UserDO user1 : user1List) {
    userList.add(user1);
}
for (UserDO user2 : user2List) {
    userList.add(user2);
}

使用JDK提供的批量拷贝效率更高。

17. 直接迭代需要使用的集合

正例

Map<Long, UserDO> userMap = ...;
for (Map.Entry<Long, UserDO> userEntry : userMap.entrySet()) {
    Long userId = userEntry.getKey();
    UserDO user = userEntry.getValue();
    ...
}

反例

Map<Long, UserDO> userMap = ...;
for (Long userId : userMap.keySet()) {
    UserDO user = userMap.get(userId);
    ...
}

在上面的例子中使用entrySet()迭代集合,更加的直接,没有多余的方法调用。

18. 使用isEmpty方法检测空

正例

List<UserDO> userList = ...;
if (userList.isEmpty()) {
    ...
}

反例

List<UserDO> userList = ...;
if (userList.size() == 0) {
    ...
}

任何isEmpty方法实现的时间复杂度都是O(1),但是某些size方法实现的时间复杂度有可能是O(n)。

19. 使用HashSet判断值存在

正例

Set<Long> adminIdSet = ...;
List<UserDO> userDOList = ...;
List<UserVO> userVOList = new ArrayList<>(userDOList.size());
for (UserDO userDO : userDOList) {
    if (adminIdSet.contains(userDO.getId())) {
        userVOList.add(transUser(userDO));
    }
}

反例

List<Long> adminIdList = ...;
List<UserDO> userDOList = ...;
List<UserVO> userVOList = new ArrayList<>(userDOList.size());
for (UserDO userDO : userDOList) {
    if (adminIdList.contains(userDO.getId())) {
        userVOList.add(transUser(userDO));
    }
}

List的contains方法普遍时间复杂度是O(n),而HashSet的时间复杂度为O(1)。如果需要频繁调用contains方法查找数据,可以先将List转换成HashSet。

20. 避免先判断存在再进行获取

正例

public static UserVO transUser(UserDO user, Map<Long, RoleDO> roleMap) {
    UserVO userVO = new UserVO();
    userVO.setId(user.getId());
    ...
    RoleDO role = roleMap.get(user.getRoleId());
    if (Objects.nonNull(role)) {
        userVO.setRole(transRole(role));
    }
}

反例

public static UserVO transUser(UserDO user, Map<Long, RoleDO> roleMap) {
    UserVO userVO = new UserVO();
    userVO.setId(user.getId());
    ...
    if (roleMap.contains(user.getRoleId())) {
        RoleDO role = roleMap.get(user.getRoleId());
        userVO.setRole(transRole(role));
    }
}

直接获取并判断空,从而避免了二次查找操作。

21. 初始化时尽量指定缓冲区大小

正例

StringBuffer buffer = new StringBuffer(1024);

反例

StringBuffer buffer = new StringBuffer();

避免多次扩容浪费时间和空间。

22. 在单线程中,尽量使用非线程安全类

正例

StringBuilder buffer = new StringBuilder(128);
buffer.append("select * from ").append(T_USER).append(" where id = ?");

反例

StringBuffer buffer = new StringBuffer(128);
buffer.append("select * from ").append(T_USER).append(" where id = ?");

使用非线程安全类,避免了不必要的同步开销。

23. 在多线程中,尽量使用线程安全类

正例

private final AtomicInteger counter = new AtomicInteger(0);
public void access(Long userId) {
    counter.incrementAndGet();
    ...
}

反例

private volatile int counter = 0;
public void access(Long userId) {
    synchronized (this) {
        counter++;
    }
    ...
}

使用线程安全类,比自己实现的同步代码更简洁更高效。

24. 尽量使用线程池减少线程开销

正例

private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10);
public void executeTask(Runnable runnable) {
    executorService.execute(runnable);
}

反例

public void executeTask(Runnable runnable) {
    new Thread(runnable).start();
}

多线程中两个必要的开销:线程的创建和上下文切换。采用线程池,可以尽量地避免这些开销。

25. 使用通用工具函数

正例

Objects.equals(name, thisName);

反例

(thisName == name) || (thisName != null && thisName.equals(name));
  • 函数式编程,业务代码减少,逻辑一目了然;
  • 通用工具函数,逻辑考虑周全,出问题概率低。

26. 使用HashSet判断主键是否存在

正例

// 查找重复字符
char[] charArray = string.toCharArray();
Set charSet = new HashSet<>(charArray.length);

if (!charSet.add(ch)) {
   return ch;
}

反例

/** 查找第一个重复字符 */
public static char findFirstRepeatedChar(String string) {
   // 检查空字符串
   if (Objects.isNull(string) || string.isEmpty()) {
       return null;
  }

   // 查找重复字符
   char[] charArray = string.toCharArray();
   Set charSet = new HashSet<>(charArray.length);
   for (char ch : charArray) {
       if (charSet.contains(ch)) {
           return ch;
      }
       charSet.add(ch);
  }

   // 默认返回为空
   return null;
}

HashSet的重要特性就是不允许值重复,我们就可以利用这一特性进行重复值的判断。

27. 使用 ThreadLocal 存储线程专有对象

正例

   prviate static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

反例

   prviate static final <Page LOCAL_PAGE = new Page();

1、保存线程上下文对象,避免多层级参数传递;

2、保存非线程安全对象,避免多线程并发调用。

28. return 一个空的集合,而不是 null

正例

public static ArrayList get(int id){
    ArrayList list = new ArrayList<>();
    list = UserService.queryById(id);
    if(Objects.Nonull(list)){
            return list;
    }
}

反例

public static ArrayList get(int id){
    ArrayList list = new ArrayList<>();
    list = UserService.queryById(id); //list == null
    return list;
}

返回null值是一种代价较高的做法,因为调用方需要做null值判断,因此查询结果为null时,返回一个空集合是非常科学的做法。

总结

以上每一个代码技术细节,或许你已经习以为常,也或许你还没有熟记在心,但是只要把这些细节聚合在一起,就是艺术级别的代码,学习无止境,愿我们一起学习,一起进步。

Bayanbulake

2021/12/05  阅读:59  主题:自定义主题1

作者介绍

Bayanbulake