来源:京东技术导读
实际开发场景中总是会遇到各种各样棘手的问题,那么开发者如何尽可能减少不必要的问题呢?本文不仅为架构师们提供了丰富的理论知识和实践经验,还通过举例和案例分析深入浅出地阐述了这些实践技能的重要性和应用方法,无论是初学者还是有一定经验的开发者,都可以从中获得一定的启示。
不同编程语言,拥有不同的特性和规约,下面就以JAVA语言为例,细数那些开发过程中容易被人忽略,但必须掌握的知识点和实践技能。
基础篇
2.1 关于命名
•蛇形命名法(snake case):又叫下划线命名法,使用下划线,单词小写,比如:my_system;
•驼峰命名法(camel case):按照单词首字母区分大小写,又可细分为大驼峰命名法和小驼峰命名法,比如:MySystem,mySystem;
•匈牙利命名法(HN case):属性+类型+描述,比如:nLength,g_cch,hwnd;
•帕斯卡命名法(Pascal case):全部首字母大写,等同于大驼峰命名法,比如:MySystem;
•脊柱命名法(spinal case):使用中划线,比如:my-system;
2.1.1 命名字典
见名知意:好的命名就是一种注释。
建议研发同学将业内常见业务场景的命名熟记,当然,已经有不少前辈总结过了,这里不再做过多的说明。这里摘录如下,可供参考:
其他类命名:Mode,Type,Invoker,Invocation,Initializer,Future,Promise,Selector,Reporter,Constants,Accessor,Generator
2.1.2 命名实践
工程通用命名规则都有哪些呢?不同的语言可能会有不同的习惯,以Java语言的驼峰命名规范举例:
5. 常量名全部大写;
规范比较抽象,先来看看不好的命名有哪些呢?
5. 大小写,数字,缩写混乱:String waitRPCResponse1 = "极易出错型";
除了标准的规范之外,在实际的开发过程中还会有一些引发困扰的实际案例。
包装类和基本数据类型的默认值是不一样的,前者是null,后者依据不同类型其默认值也不一样。从数据严谨的角度来讲,包装类的null值能够表示额外信息,从而更加安全。比如可以规避基本类型的自动拆箱,导致的NPE风险以及业务逻辑处理异常风险。所以成员变量必须使用包装数据类型,基本数据类型则在局部变量的场景下使用。
publicget (); public void set ( p)
public boolean is(); public void set (boolean p)
由于各种RPC框架和对象序列化工具对于布尔类型变量的处理方式存在差异,就容易造成代码移植性问题。最常见的json序列化库Jackson和Gson之间就存在兼容性问题,前者是通过通过反射遍历出该类中的所有getter方法,通过方法名截取获得到对象的属性,后者则是通过反射直接遍历该类中的属性。为了规避这种差异对业务的影响,建议所有成员变量都不要以is开头,防止序列化结果出现不预知的情况发生。
(1)一类是同一个jar包出现了多个不同的版本。应用选择了错误的版本导致jvm加载不到需要的类或者加载了错误版本的类;(借助maven管理工具相对容易解决)
public class SkuKey implements Serializable { @JsonProperty(value = "sn") @ApiModelProperty(name = "stationNo", value = " 门店编号", required = true) private Long stationNo; @JsonProperty(value = "si") @ApiModelProperty(name = "skuId", value = " 商品编号", required = true) private Long skuId; // 省略get/set方法}2.2 关于注释
1. 冗余式:如果一个函数,读者能够很容易的就读出来代码要表达的意思,注释就是多余的;
2. 错误式:如果注释的不清楚,甚至出现歧义,那还不如不写;
3. 签名式:类似“add by liuhuiqing 2023-08-05”这种注释,容易过期失效而且不太可信(不能保证所有人每次都采用这种方式注释),其功能完全可以由git代码管理工具来实现;
4. 长篇大论式:代码块里,夹杂了大篇幅的注释,不仅影响代码阅读,而且维护困难;
5. 非本地注释:注释应该在离代码实现最近的地方,比如:被调用的方法注释就由方法本身来维护,调用方无需对方法做详细的说明;
2.3 关于分层
在ISO(International Standardization Organization)于1981年制定网络通信七层模型(Open System Interconnection Reference Model,OSI/RM)之前,计算机网络中存在众多的体系结构,其中以IBM公司的SNA(系统网络体系结构)和DEC公司的DNA(DigitalNetworkArchitecture)数字网络体系结构最为著名。
最早之前,各个厂家提出的不同标准都是以自家设备为基础的,用户在选择产品的时候就只能用同一家公司的,因为不同公司间大家的标准不一样,工作方式也可能不一样,结果就是不同厂商的网络产品间,可能会出现不兼容的情况。如果说同一家的公司的产品都能满足用户的需求的话,那就看哪家公司实力强点,实力强的,用户粘性高的,用户自然也不会说什么,问题是一家公司并不是对所有的产品都擅长。这就会导致厂商和用户都面临着痛苦的煎熬。类比一下当前手机充电接口协议(Micro USB接口、Type- c接口、Lightning接口),手头总是要备有各种充电线的场景,就能深刻理解标准的意义了。
2.4 小结
实践篇
下面就从程序的扩展性,维护性,安全性以及性能等几个重要质量指标,来学习那些经典的实践案例。
3.1 类定义
3.1.1 常量定义
常量是一种固定值,不会在程序执行期间发生改变。你可以使用枚举(Enum)或类(Class)来定义常量。
如果你需要定义一组相关的常量,那么使用枚举更为合适。枚举从安全性和可操作性(支持遍历和函数定义)上面拥有更大的优势。
public enum Color { RED, GREEN, BLUE;}如果你只需要定义一个或少数几个只读的常量,那么使用类常量更为简洁和方便。
public class MyClass { public static final int MAX_VALUE = 100;}public abstract class ObjectHelper {public static boolean isEmpty(String str) {return str == null || str.length() == 0;}}
为了实现不需要实例化对象的约束,开发者在类定义时,最好加上abstract关键字进行声明限定,这也是为什么spring等开源工具类大都使用abstract关键字修饰。
3.1.3 JavaBean
JavaBean的定义有两种常见实现方式:手动编写和自动生成。
public class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}}
import lombok.Data;@NoArgsConstructor@Data@Accessors(chain = true)public class Person {private String name;private int age;}
public final class String implements Serializable, Comparable, CharSequence { }
java.lang.Stringjava.lang.Mathjava.lang.Booleanjava.lang.Characterjava.util.Datejava.sql.Datejava.lang.Systemjava.lang.ClassLoader
1. 直接作为参数传递给方法或构造函数;
2. 用于实现某个接口或抽象类的匿名实例;
public class Example { public static void main(String[] args) { // 创建一个匿名内部类 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello, World!"); } }; // 调用匿名内部类的方法 runnable.run(); }}/** * 关键定义的类是不可变类 * 将所有成员变量通过参数的形式定义 * 默认会生成全部参数的构造方法 * @param name * @param age */public record Person(String name, int age) { public Person{ if(name == null){ throw new IllegalArgumentException("提供紧凑的方式进行参数校验"); } } /** * 定义的类中可以定义静态方法 * @param name * @return */ public static Person of(String name) { return new Person(name, 18); }}Person person = new Person("John", 30);// Person person = Person.of("John");String name = person.name();int age = person.age();public ListsortPeopleByAge(List people) { record Data(Person person, int age){};return people.stream().map(person -> new Data(person, computAge(person))).sorted((d1, d2) -> Integer.compare(d2.age(), d1.age())).map(Data::person).collect(toList());}public int computAge(Person person) {return person.age() - 1;}
1. final修饰类,这样类就无法被继承了;
2. package-private类,可以控制只能被同一个包下的类继承;
sealed class SealedClass permits SubClass1, SubClass2 {}class SubClass1 extends SealedClass {}class SubClass2 extends SealedClass {}
3.2 方法定义
public class MyClass {private int myInt;private String myString;// 构造方法public MyClass(int myInt, String myString) {this.myInt = myInt;this.myString = myString;}}
class Animal {public void makeSound() {System.out.println("Animal is making a sound");}}class Cat extends Animal {@Overridepublic void makeSound() {System.out.println("Meow");}}public class Main {public static void main(String[] args) {Animal myCat = new Cat();myCat.makeSound(); // 输出 "Meow"}}
public class Calculator {public int add(int a, int b) {return a + b;}public double add(double a, double b) {return a + b;}}public class Main {public static void main(String[] args) {Calculator calculator = new Calculator();int result1 = calculator.add(2, 3);double result2 = calculator.add(2.5, 3.5);System.out.println(result1); // 输出 5System.out.println(result2); // 输出 6.0}}
Java 8 引入了 Lambda 表达式,可以用来实现类似匿名方法的功能。Lambda 表达式是一种匿名函数,可以作为参数传递给方法,或者直接作为一个独立表达式使用。
public static void main(String args[]) {Listnames = Arrays.asList("hello", "world"); // 使用 Lambda 表达式作为参数传递给 forEach 方法names.forEach((String name) -> System.out.println("Name: " + name));// 使用 Lambda 表达式作为独立表达式使用PredicatenameLengthGreaterThan5 = (String name) -> name.length() > 5; boolean isLongName = nameLengthGreaterThan5.test("John");System.out.println("Is long name? " + isLongName);}
3.3 对象定义
1. 控制资源的使用:通过线程同步来控制资源的并发访问。
2. 控制实例产生的数量:达到节约资源的目的。
public enum Singleton {INSTANCE;public void someMethod() {// ...其他代码...}}
1. 将对象的状态存储在不可变对象中:String、Integer等就是内置的不可变对象类型;
2. 将对象的状态存储在final变量中:final变量一旦被赋值就不能被修改;
Collections.unmodifiableList(new ArrayList<>());
public class Pair {public final A first;public final B second;public Pair(A a, B b) {this.first = a;this.second = b;}public A getFirst() {return first;}public B getSecond() {return second;}}
三元组实现
public class Triplet extends Pair{public final C third;public Triplet(A a, B b, C c) {super(a, b);this.third = c;}public C getThird() {return third;}public static void main(String[] args) {// 表示姓名,性别,年龄Triplettriplet = new Triplet("John","男",18); // 获得姓名String name = triplet.getFirst();}}
多元组实现
public class Tuple{ private final E[] elements;public Tuple(E... elements) {this.elements = elements;}public E get(int index) {return elements[index];}public int size() {return elements.length;}public static void main(String[] args) {// 表示姓名,性别,年龄Tupletuple = new Tuple<>("John", "男", "18"); // 获得姓名String name = tuple.get(0);}}
1. 存储多个数据元素:Tuple可以存储多个不同类型的数据元素,这些元素可以是基本类型、对象类型、数组等;
2. 简化代码:Tuple可以使代码更加简洁,减少重复代码的编写。通过Tuple,开发者可以将多个变量打包成一个对象,从而减少了代码量;
3. 提高代码可读性:Tuple可以提高代码的可读性。通过Tuple,开发者可以将多个变量打包成一个对象,从而使代码更加易读;
NamedTuple namedTuple = Tuples.named("person", "name", "age");1. 尽量重用对象。由于系统不仅要花时间生成对象,以后可能还需花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响,重用对象的策略有缓存对象,也可以针对具体场景进行定向优化,比如使用StringBuffer代替字符串拼接的方式;
2. 尽量使用局部变量。调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快。其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢;
值类型(value types)的概念是表示纯数据聚合,这会删除常规对象的功能。因此只有纯数据而没有身份。当然,这意味着也失去了使用对象标识可以实现的功能。由于不再有对象标识,可以放弃指针,改变值类型的一般内存布局。下面将对象引用和值类型内存布局进行对比。

图1.
•性能增强通过展平对象图和移除间接来解决。这将获得更高效的内存布局和更少的分配和垃圾回收。
截止到2023年9月,Valhalla 项目仍在进行中,还没有正式版本的发布,这一创新项目值得期待的。
本文总结了软件开发过程中经常用到的基础常识,分为基础篇和实践篇两个篇章,其中基础篇中着重讲述了类,方法,变量的命名规范以及代码注释好坏的评判标准。实践篇中从类,方法以及对象三个层面分析了常见的技术概念和落地实践,希望这些常识能够为读者带来一些思考和帮助。