普通的工程师堆砌代码,优秀的工程师优雅代码,卓越的工程师简化代码。如何写出优雅整洁易懂的代码是一门学问,也是软件工程实践里重要的一环。--来自网络
软件质量,不但依赖于架构及项目管理,更与代码质量紧密相关。简洁高效的代码不但易于阅读,更能避免潜在BUG与风险,提高代码质量。近期,一位Oracle程序员在Hacker News上吐槽自己的工作,引起了热议。
这个工程师的核心痛点是,Oracle经历长期的产品线迭代,代码异常庞大、逻辑复杂,整个代码中充斥着神秘的宏命令。每新增一个特性或者修复BUG,该工程师都需要大量的调研,小心谨慎的进行着日常的工作。而Oracle每次的版本发布都经历数百万次的测试,脑补一下,如噩梦一般。那么我们应该如何编写简洁高效的代码呢?其实业内有过很多相关书籍,比如经典的书籍有《代码整洁之道》、《编写可读代码的艺术》、《重构:改善既有代码的设计》,可用于修炼内功。以及我们有严格的代码规范以及方便的静态代码扫描工具,可用于加强研发代码质量能力。
其实代码规范和静态代码扫描工具能够帮助我们完成很多代码简洁的工作。诸如:注释、命名、方法、异常、单元测试等多个方面。但却无法总结了一些代码简洁最佳实践,其实Java是面向对象语音,而面向对象的特征是封装、继承、多态,巧妙的运用这三大特性、理解Java的一些关键字特性、语音特性、阅读JDK源码,就可以写出相对简洁的代码了。
// 修改前 if(list.size()>0) { return true; } else { return false; } // 修改后 return list.size()>0; 复制代码
// 修改前 public List<Map<String, Object>> queryList(Map<String, Object> params) { List<Map<String, Object>> list = null; try { list = mapper.queryList(params); } catch (Throwable e) { throw new RuntimeException("ERROR", e); } return list; } // 修改后 public List<Map<String, Object>> queryList(Map<String, Object> params) { try { return mapper.queryList(params); } catch (Throwable e) { throw new RuntimeException("ERROR", e); } } 复制代码
// 修改前 if (0 == retCode) { sendMessage("A001", "Process Success", outResult); } else { sendMessage("A001", "Process Failure", outResult); } // 修改后 1 String message = (0 == retCode ? "Process Success" : "Process Failure"); sendMessage("A001", message, outResult); // 修改后 2 sendMessage("A001", messageFromRetCode(retCode), outResult); 复制代码
// 修改前 String uuid = UUIDUtils.getUUID(); String date = DateTimeUtils.getCurrDt(); String time = DateTimeUtils.getCurrTm(); Order order = new Order(); order.setSrUsrId(map.get("srcUsrId")); // 省略几十个set ... order.setTmsDate(date); order.setTmsCte(time); order.setUuid(uuid); list.add(order); // 修改后 list.add(buildOrder(map)); public Order buildOrder(Map<String,String> map){ Order order = new Order(); order.setSrUsrId(map.get("srcUsrId")); // 省略几十个set ... String date = DateTimeUtils.getCurrDt(); order.setTmsDate(date); order.setTmsCte(DateTimeUtils.getCurrTm()); String uuid = UUIDUtils.getUUID(); order.setUuid(uuid); return order; } 复制代码
JAVA8特性“函数式编程”,使用Lambdas我们能做到什么?
// 修改前 public static void test1() { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); for (int number : numbers) { System.out.println(number); } } // 修改后1 // 使用lambda表达式以及函数操作(functional operation) public static void test2(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); numbers.forEach((Integer value)-> System.out.println(value)); } // 修改后2 //在Java8中使用双冒号操作符(double colon operator) public static void test3(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); numbers.forEach(System.out::println); } 复制代码
上述代码是传统方式的遍历一个List的写法,简单来说主要有3个不足:
而使用函数式编程能规避上面的三个问题:
在Java8中,接口中的方法可以被实现,用关键字 default 作为修饰符来标识,接口中被实现的方法叫做 default 方法。使用default方法,当接口发生改变的时候,实现类不需要做改动,所有的子类都会继承 default 方法。
public class Test1 { public static void main(String[] args) { Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; System.out.println(formula.calculate(100)); // 100.0 System.out.println(formula.sqrt(16)); // 4.0 } } interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } 复制代码
当一个接口扩展另外一个包含默认方法的接口的时候,有以下3种处理方式。
Java8中新增了LocalDate和LocalTime接口,为什么要搞一套全新的处理日期和时间的API?因为旧的java.util.Date实在是太难用了。
当然,LocalDateTime才能同时包含日期和时间。
新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用java.util.Date配合Calendar要写好多代码,而且一般的开发人员还不一定能写对。
Clock c = Clock.systemDefaultZone(); System.out.println(System.currentTimeMillis()); System.out.println(c.millis()); Date date = Date.from(c.instant()); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(date)); // instant精确到纳秒,比原来的date的毫秒要更精确 // 获取当前时间 Instant in = Instant.now(); System.out.println(in); // 将现在的时间增加3小时2分,将产生新的实例 Instant in1 = in.plus(Duration.ofHours(3).plusMinutes(2)); System.out.println(in1); System.out.println(in1 == in); // 关于计算的例子 in.minus(5, ChronoUnit.DAYS);// 计算5天前 in.minus(Duration.ofDays(5));// 计算5天前 // 计算两个Instant之间的分钟数 long diffAsMinutes1 = ChronoUnit.MINUTES.between(in, in1); // 方法2 System.out.println(diffAsMinutes1); // instant是可比较的,有isAfter和isBefore System.out.println(in.isAfter(in1)); System.out.println(in.isBefore(in1)); 复制代码
// 取当前日期 LocalDate today = LocalDate.now(); System.out.println(today); // 获得2005年的第86天 (27-Mar-2005) LocalDate localDate = LocalDate.ofYearDay(2005, 86); System.out.println(localDate); // 根据年月日取日期 2013年8月10日 localDate = LocalDate.of(2013, Month.AUGUST, 10); localDate = LocalDate.of(2013, 8, 10); // 根据字符串取 LocalDate.parse("2014-02-28"); // 严格按照ISO yyyy-MM-dd验证,02写成2都不行 LocalDate.parse("2014-02-29"); // 无效日期无法通过:DateTimeParseException: Invalid date // 取本月第1天: LocalDate firstDayOfThisMonth = today.with(TemporalAdjusters.firstDayOfMonth()); // 取本月第2天: LocalDate secondDayOfThisMonth = today.withDayOfMonth(2); // 取本月最后一天,再也不用计算是28,29,30还是31: LocalDate lastDayOfThisMonth = today.with(TemporalAdjusters.lastDayOfMonth()); // 取下一天: LocalDate firstDayOf2015 = lastDayOfThisMonth.plusDays(1); // 变成了2015-01-01 // 取2015年1月第一个周一,这个计算用Calendar要死掉很多脑细胞: LocalDate firstMondayOf2015 = LocalDate.parse("2015-01-01") .with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); // LocalTime LocalTime now = LocalTime.now(); // 带纳秒 LocalTime now1 = LocalTime.now().withNano(0); // 清除纳秒 System.out.println(now); System.out.println(now1); LocalTime localTime = LocalTime.of(22, 33); System.out.println(localTime); // 返回一天中的第4503秒 localTime = LocalTime.ofSecondOfDay(4503); System.out.println(localTime); LocalDateTime localDateTime0 = LocalDateTime.now(); System.out.println(localDateTime0); // 当前时间加上25小时3分钟 LocalDateTime inTheFuture = localDateTime0.plusHours(25).plusMinutes(3); System.out.println(inTheFuture); // 同样也可以用在localTime和localDate中 System.out.println(localDateTime0.toLocalTime().plusHours(25).plusMinutes(3)); System.out.println(localDateTime0.toLocalDate().plusMonths(2)); 复制代码
Stream是对集合的包装,通常和lambda一起使用。使用lambdas可以支持许多操作。如 map,filter,limit,sorted,count,min,max,sum,collect等等。 同样,Stream使用懒运算,他们并不会真正地读取所有数据。遇到像getFirst()这样的方法就会结束链式语法,通过下面一系列例子介绍:比如我有个Person类,就是一个简单的pojo, 针对这个对象,我们可能有这样一系列的运算需求。
class Person{ private String name, job, gender; private int salary, age; // 省略若干get/set方法及构造方法 ... } 复制代码
List<Person> persons = new ArrayList<Person>() { private static final long serialVersionUID = 1L; { add(new Person("张三", "Java", "female", 25, 1000)); add(new Person("李四", "Java", "male", 29, 1200)); add(new Person("王五", "测试", "female", 25, 1400)); add(new Person("赵六", "Java", "male", 31, 1800)); add(new Person("张三三", "设计", "male", 33, 1900)); add(new Person("李四四", "需求", "female", 30, 2000)); add(new Person("王五五", "Java", "female", 29, 2100)); add(new Person("赵六六", "Java", "male", 43, 2800)); } }; 复制代码
Consumer<Person> print = e -> System.out.println(e.toString()); persons.forEach(print); 复制代码
Consumer<Person> raise = e -> e.setSalary(e.getSalary()/100*10+e.getSalary()); persons.forEach(raise); persons.forEach(print); 复制代码
persons.stream().filter((p) -> (p.getSalary()< 1500)).forEach(print); 复制代码
Predicate<Person> salaryPredicate = e -> e.getSalary() > 2000; Predicate<Person> jobPredicate = e -> "Java".equals(e.getJob()); Predicate<Person> agePredicate = e -> e.getAge() >= 29; Predicate<Person> genderPredicate = e -> "female".equals(e.getGender()); persons.stream().filter(salaryPredicate) .filter(jobPredicate) .filter(agePredicate) .filter(genderPredicate) .forEach(print); 复制代码
persons.stream().filter(genderPredicate).limit(2).forEach(print); 复制代码
persons.stream().sorted((p1,p2)-> (p1.getAge() - p2.getAge())) //.sorted((p1,p2)->(p1.getName().compareTo(p2.getName()))) .forEach(print); 复制代码
System.out.println(persons.stream().min((p1,p2)->(p1.getSalary()-p2.getSalary())).get().toString()); System.out.println(persons.stream().max((p1,p2)->(p1.getSalary()-p2.getSalary())).get().toString()); 复制代码
System.out.println("所有人的工资总和:"+ persons.stream().parallel().mapToInt(p - > p.getSalary()).sum()); 复制代码
String str = persons.stream().map(Person::getName).collect(Collectors.joining(";")); System.out.println(str); TreeSet<String> ts = persons.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new)); System.out.println("ts.toString():"+ts.toString()); Set<String> set = persons.stream().map(Person::getName).collect(Collectors.toSet()); System.out.println("set.toString():"+set.toString()); 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); IntSummaryStatistics stats = numbers .stream() .mapToInt((x) -> x) .summaryStatistics(); System.out.println("List中最大的数字 : " + stats.getMax()); System.out.println("List中最小的数字 : " + stats.getMin()); System.out.println("所有数字的总和 : " + stats.getSum()); System.out.println("所有数字的平均值 : " + stats.getAverage()); 复制代码
List<Integer> numbers1 = Arrays.asList(9, 10, 3, 4, 7, 3, 4); List<Integer> distinct = numbers1.stream().distinct().collect(Collectors.toList()); System.out.println(distinct); 复制代码
//sumAll算法很简单,完成的是将List中所有元素相加。 public static int sumAll(List<Integer> numbers) { int total = 0; for (int number : numbers) { total += number; } return total; } 复制代码
sumAll算法很简单,完成的是将List中所有元素相加。某一天如果我们需要增加一个对List中所有偶数求和的方法sumAllEven,那么就产生了sumAll2,如下:
public static int sumAll2(List<Integer> numbers) { int total = 0; for (int number : numbers) { if (number % 2 == 0) { total += number; } } return total; } 复制代码
又有一天,我们需要增加第三个方法:对List中所有大于3的元素求和,那是不是继续加下面的方法呢?sumAll3
public static int sumAll3(List<Integer> numbers) { int total = 0; for (int number : numbers) { if (number > 3) { total += number; } } return total; } 复制代码
观察这三个方法我们发现,有很多重复内容,唯一不同的是方法中的if条件不一样(第一个可以看成if(true)),如果让我们优化,可能想到的第一种重构就是策略模式吧,代码如下:
public interface Strategy { public boolean test(int num); } public class SumAllStrategy implements Strategy { @Override public boolean test(int num) { return true; } } public class SumAllEvenStrategy implements Strategy { @Override public boolean test(int num) { return num % 2 == 0; } } public class SumAllGTThreeStrategy implements Strategy { @Override public boolean test(int num) { return num > 3; } } public class BodyClass { private Strategy stragegy = null; private final static Strategy DEFAULT_STRATEGY = new SumAllStrategy(); public BodyClass() { this(null); } public BodyClass(Strategy arg) { if (arg != null) { this.stragegy = arg; } else { this.stragegy = DEFAULT_STRATEGY; } } public int sumAll(List<Integer> numbers) { int total = 0; for (int number : numbers) { if (stragegy.test(number)) { total += number; } } return total; } } //调用 BodyClass bc = new BodyClass(); bc.sumAll(numbers); 复制代码
这无疑使用设计模式的方式优化了冗余代码,但是可能要额外增加几个类,以后扩展也要新增,下面看看使用lambda如何实现,声明方法:第一个参数还是我们之前传递的List数组,第二个看起来可能有点陌生,通过查看jdk可以知道,这个类是一个谓词(布尔值的函数)
public static int sumAllByPredicate(List<Integer> numbers, Predicate<Integer> p) { int total = 0; for (int number : numbers) { if (p.test(number)) { total += number; } } return total; } //调用: sumAllByPredicate(numbers, n -> true); sumAllByPredicate(numbers, n -> n % 2 == 0); sumAllByPredicate(numbers, n -> n > 3); 复制代码
代码是不是比上面简洁了很多?语义也很明确,重要的是不管以后怎么变,都可以一行代码就修改了。。。万金油啊。
JAVA8 还推出了很多特性,来简化代码。比如String.join函数、Objects类、Base64编码类。
String joined = String.join("/", "usr","local","bin"); String joided1="usr/"+"local/"+"bin/"; System.out.println(joined); String ids = String.join(", ", ZoneId.getAvailableZoneIds()); System.out.println(ids); 复制代码
String aa = null; Objects.requireNonNull(aa," aa must be not null"); Object a = null; Object b = new Object(); if(a.equals(b)){ } if(Objects.equals(a, b)){ } 复制代码
Base64.Encoder encoder = Base64.getEncoder(); Base64.Decoder decoder = Base64.getDecoder(); String str = encoder.encodeToString("你好".getBytes(StandardCharsets.UTF_8)); System.out.println(str); System.out.println(new String(decoder.decode(str),StandardCharsets.UTF_8)); 复制代码
好的代码需要不停的打磨,作为一个优秀的工程师,我们应该严格遵守,每次提交的代码要比迁出的时候更好。经常有人说,作为工程师一定要有团队精神,但这种精神并不是说说而已的,需要实际的行动来体现的。设计模式、JDK的新特性都是我们可以借助的经验,编码完成后思考一下,还可不可以在简化、优化,不要成为一个“作恶”的工程师。
马铁利,随行付架构部负责人 & TGO鲲鹏会北京分会会员,10年全栈工程师,擅长微服务分布式架构设计。主要负责随行付架构部日常管理;参与构建微服务平台周边基础设施及中间件;负责随行付对外开源等事宜。