架构师日记-聊聊开发必掌握的那些实践技能 | 京东云技术团队

一 引言

尽管软件开发一直致力于追求高效、可读性强、易于维护的特性,但这些特性却像是一个不可能三角,相互交织,此消彼长。就像底层语言(如汇编和C语言)能够保持高效的运行性能,但在可读性和维护性方面却存在短板和劣势;而高级语言(如Java和Python)在可读性和可维护性方面表现出色,但在执行效率方面却存在不足。

构建语言生态的优势,弥补其存在短板,始终是编程语言的一个演进方向。

不同编程语言,拥有不同的特性和规约,下面就以JAVA语言为例,细数那些开发过程中容易被人忽略,但必须掌握的知识点和实践技能。

二 基础篇

1999年,美国太空总署(NASA)的火星任务失败:在这次任务中, 火星气候探测者号上的飞行系统软件使用公制单位牛顿计算推进器动力,而地面人员输入的方向校正量和推进器参数则使用英制单位磅力,导致探测器进入大气层的高度有误,最终瓦解碎裂。

这是由于国际标准(牛)和本土化(磅)的冲突导致的一起事故。由此引出了程序需要关注可维护性这个话题,由于软件生产往往需要多人协作,可维护性正是协作共识里的重要一环。关于这方面,让人最容易想到的就是命名和注释两个方面了,下面就展开来探讨一下。

2.1 关于命名

按照阅读习惯,程序的变量命名法都需要克服单词间的空格问题,从而把不同单词串连起来,最终达到创造出一种易于阅读的新“单词”的效果。常见的命名方法有以下几种:

  • 蛇形命名法(snake case):又叫下划线命名法,使用下划线,单词小写,比如:my_system;
  • 驼峰命名法(camel case):按照单词首字母区分大小写,又可细分为大驼峰命名法和小驼峰命名法,比如:MySystem,mySystem;
  • 匈牙利命名法(HN case):属性+类型+描述,比如:nLength,g_cch,hwnd;
  • 帕斯卡命名法(Pascal case):全部首字母大写,等同于大驼峰命名法,比如:MySystem;
  • 脊柱命名法(spinal case):使用中划线,比如:my-system;
  • 自由命名法(studly caps):大小写混杂,无简明规则,比如:mySYSTEM,MYSystem;

按照受众量与知名程度排名,驼峰命名法和蛇形命名法更受到大家的欢迎,毕竟它们在可读性,易写性等方面比较有优势。

2.1.1 命名字典

见名知意:好的命名就是一种注释。

建议研发同学将业内常见业务场景的命名熟记,当然,已经有人帮我们总结过了,这里不再做过多的说明。这里摘录如下,可供参考:

管理类命名:Bootstrap,Starter,Processor,Manager,Holder,Factory,Provider,Registrar,Engine,Service,Task

传播类命名:Context,Propagator

回调类命名:Handler,Callback,Trigger,Listener,Aware

监控类命名:Metric,Estimator,Accumulator,Tracker

内存管理类命名:Allocator,Chunk,Arena,Pool

过滤检测类命名:Pipeline,Chain,Filter,Interceptor,Evaluator,Detector

结构类命名:Cache,Buffer,Composite,Wrapper,Option, Param,Attribute,Tuple,Aggregator,Iterator,Batch,Limiter

常见设计模式命名:Strategy,Adapter,Action,Command,Event,Delegate,Builder,Template,Proxy

解析类命名:Converter,Resolver,Parser,Customizer,Formatter

网络类命名:Packet,Encoder、Decoder、Codec,Request,Response

CRUD命名:Controller,Service,Repository

辅助类命名:Util,Helper

其他类命名:Mode,Type,Invoker,Invocation,Initializer,Future,Promise,Selector,Reporter,Constants,Accessor,Generator

2.1.2 命名实践

工程通用命名规则都有哪些呢?不同的语言可能会有不同的习惯,以Java语言的驼峰命名规范举例:

  1. 项目名全部小写;

  2. 包名全部小写;

  3. 类名首字母大写,其余组成词首字母依次大写;

  4. 变量名,方法名首字母小写,如果名称由多个单词组成,除首字母外的每个单词首字母都要大写;

  5. 常量名全部大写;

规范比较抽象,先来看看不好的命名有哪些呢?

  1. 自带混淆功能的变量名:String zhrmghg = “极致缩写型”;

  2. 没有意义的万能变量名:String a,b,c=“爱谁谁型”;

  3. 长串拼音变量名:String HuaBuHua = “考古型”;

  4. 各种符号混用:String $my_first_name_ = “打死记不住型”;

  5. 大小写,数字,缩写混乱:String waitRPCResponse1 = “极易出错型”;

除了标准的规范之外,在实际的开发过程中还会有一些困扰我们的实际案例。

1. 在定义一个成员变量的时候,到底是使用包装类型还是使用基本数据类型呢?

包装类和基本数据类型的默认值是不一样的,前者是null,后者依据不同类型其默认值也不一样。从数据严谨的角度来讲,包装类的null值能够表示额外信息,从而更加安全。比如可以规避基本类型的自动拆箱,导致的NPE风险以及业务逻辑处理异常风险。所以成员变量必须使用包装数据类型,基本数据类型则在局部变量的场景下使用。

2. 为什么不建议布尔类型的成员变量以is开头?

关于Java Bean中的getter/setter方法的定义其实是有明确的规定的,根据JavaBeans™ Specification规定,如果是普通的参数,命名为propertyName,需要通过以下方式定义其setter/getter:

    public <PropertyType> get<PropertyName>();
    public void set<PropertyName>(<PropertyType> p)

但是,布尔类型的变量propertyName则是另外一套命名原则的:

    public boolean is<PropertyName>();
    public void set<PropertyName>(boolean p)

由于各种RPC框架和对象序列化工具对于布尔类型变量的处理方式存在差异,就容易造成代码移植性问题。最常见的json序列化库Jackson和Gson之间就存在兼容性问题,前者是通过通过反射遍历出该类中的所有getter方法,通过方法名截取获得到对象的属性,后者则是通过反射直接遍历该类中的属性。为了规避这种差异对业务的影响,建议所有成员变量都不要以is开头,防止序列化结果出现不预知的情况发生。

3. 看看单词大小写能引起的哪些副作用?

JAVA语言本身是区分大小写的,但是在用文件路径、文件名对文件进行操作时,这里的文件名和路径是不区分大小写的,这是因为文件系统不区分大小写。典型的场景就是我们通过git等代码管理平台时,将package路径里的大写的文件名称,修改为小写时,git是无法更新的,为了规避不必要的麻烦,这里建议包路径统一使用小写单词,多个单词通过路径层次来进行定义。

4. 不同jar包里的类也会出现冲突问题?

  1. 一类是同一个jar包出现了多个不同的版本。应用选择了错误的版本导致jvm加载不到需要的类或者加载了错误版本的类;(借助maven管理工具相对容易解决)

  2. 另一类是不同的jar包出现了类路径相同的类,同样的类出现在不同的依赖jar里,由于jar加载的先后顺序导致了JVM加载了错误版本的类;(比较难以解决)

这里着重介绍第二种情况,这种情况容易出现在系统拆分重构时,将原有的项目进行了复制,然后删减,导致部分工具或者枚举类和原有的路径和命名都一样,当第三方调用方同时依赖了这两个系统时,就容易为以后的迭代埋下坑。要规避此类问题,一定要为系统起一个独一无二的package路径。

补充:如果依赖的都是第三方的库,存在着类冲突时,可以通过引入第三方库jarjar.jar,修改其中某个冲突jar文件的包名,以此来解决jar包冲突。

5. 在变量命名的可读性和占用资源(内存,带宽)方面,如何去做权衡?

可以通过对象序列化工具为突破口,以常见的Json(Jackson)序列化方式来举例:

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方法
}

其中@JsonProperty注解的作用就是将JavaBean中的普通属性在序列化的时候,重新命名成指定的新的名字。而这一实现对于业务实现没有影响,依然以原来的命名操作为准,只在对外RPC需要序列化和反序列化的过程生效。如此,比较好的解决了可读性和资源占用的冲突问题。

6. 对外提供服务的入参和出参,我们是用class对象,还是Map容器?

从灵活性的角度看,Map容器稳定且更灵活。从稳定性和可读性上来看,Map容器是个黑盒子,不知道里面有什么,得有辅助的详细说明文档才能协作,由于维护文档的动作往往与工程代码是分开的,这中机制就会导致信息的准确性和实时性很难得到保障。所以还是建议使用class结构对象维护出入参结构。

2.2 关于注释

注释是程序员和阅读者之间交流的重要手段,是对代码的解释和说明,好的注释可以提高软件的可读性,减少维护软件的成本。

2.2.1 好的注释

分层次 :按照系统,包,类,方法,代码块,代码行等不同粒度,各有侧重点的进行注释说明。

  1. 系统注释:通过README.md文件体现宏观的功能和架构实现;

  2. 包注释:通过package-info文件体现模块职责边界,另外该文件也支持声明友好类,包常量以及为标注在包上的注解(Annotation)提供便利;

  3. 类注释:主要体现功能职责,版本支持,作者归属,应用示例等相关信息;

  4. 方法注释:关注入参,出参,异常处理声明,使用场景举例等相关内容;

  5. 代码块和代码行注释:主要体现逻辑意图,闭坑警示,规划TODO,放大关注点等细节内容;

有规范 :好的代码优于大量注释,这和我们常说的“约定大于配置”是相同的道理。借助swagger等三方库实现 注解即接口文档 ,是一个不错的规范方式;

2.2.2 坏的注释

为了能使注释准确清晰的表达出功能逻辑,注释的维护是有相当的维护成本的,所以注释并不是越多,越详细越好。下面就举一些坏的注释场景,辅助理解:

  1. 冗余式:如果一个函数,读者能够很容易的就读出来代码要表达的意思,注释就是多余的;

  2. 错误****式:如果注释的不清楚,甚至出现歧义,那还不如不写;

  3. 签名式:类似“add by liuhuiqing 2023-08-05”这种注释,容易过期失效而且不太可信(不能保证所有人每次都采用这种方式注释),其功能完全可以由git代码管理工具来实现;

  4. 长篇大论式:代码块里,夹杂了大篇幅的注释,不仅影响代码阅读,而且维护困难;

  5. 非本地注释:注释应该在离代码实现最近的地方,比如:被调用的方法注释就由方法本身来维护,调用方无需对方法做详细的说明;

  6. 注释掉的代码:无用的代码应该删除,而不是注释。历史记录交给git等代码管理工具来维护;

2.3 关于分层

系统分层设计的主要目是通过分离关注点,来降低系统的复杂度,同时提高可复用性和降低维护成本。所以懂得分层的概念,很大程度上系统的可维护性就有了骨架。

2.3.1 系统分层

在ISO((International Standardization Organization))于1981年制定网络通信七层模型(Open System Interconnection Reference Model,OSI/RM)之前,计算机网络中存在众多的体系结构,其中以IBM公司的SNA(系统网络体系结构)和DEC公司的DNA(DigitalNetworkArchitecture)数字网络体系结构最为著名。

最早之前,各个厂家提出的不同标准都是以自家设备为基础的,用户在选择产品的时候就只能用同一家公司的,因为不同公司间大家的标准不一样,工作方式也可能不一样,结果就是不同厂商的网络产品间,可能会出现不兼容的情况。如果说同一家的公司的产品都能满足用户的需求的话,那就看哪家公司实力强点,实力强的,用户粘性高的,用户自然也不会说什么,问题是一家公司并不是对所有的产品都擅长。这就会导致厂商和用户都面临着痛苦的煎熬。类比一下当前手机充电接口协议(Micro USB接口、Type- c接口、Lightning接口),手头总是要备有各种充电线的场景,就能深刻理解标准的意义了。

2.3.2 软件伸缩性

软件伸缩性指的是软件系统在面对负载压力时,能够保持原有性能并扩展以支持更多任务的能力。

伸缩性可以有两个方面,垂直伸缩性和水平伸缩性,垂直伸缩性是通过在同一个业务单元中增加资源来提高系统的吞吐量,比如增加服务器cpu的数量,增加服务器的内存等。水平伸缩性是通过增加多个业务单元资源,使得所有的业务单元逻辑上就像是一个单元一样。比如ejb分布式组件模型,微服务组件模型等都属于此种方式。

软件系统在设计时需要考虑如何进行有效的伸缩性设计,以确保在面对负载压力时能够提供足够的性能支持。

系统分层从伸缩性角度看,更多的属于水平伸缩性的范畴。在J2EE系统开发当中,我们普遍采用了分层构架的方式,一般分为表现层,业务层和持久层。采用分层以后,因为层与层之间通信会引来额外的开销,所以给我们软件系统带来的就是每个业务处理开销会变大。

既然采用分层会带来额外的开销,那么我们为什么还要进行分层呢?

这是因为单纯依靠堆硬件资源的垂直伸缩方式来提高软件性能和吞吐是有上限的,而且随着系统规模的扩大,垂直伸缩的代价也将变得非常昂贵。当采用了分层以后,虽然层与层之间带来了通信开销,但是它有利于各层的水平伸缩性,并且各个层都可以进行独立的伸缩而不会影响到其它的层。也就是说当系统要应对更大的访问量的时候,我们可以通过增加多个业务单元资源来增加系统吞吐量。

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;
}
3.1.2 工具类

工具类通常包含具有通用性的、某一非业务领域内的公共方法,不需要配套的成员变量,仅仅是作为工具方法被使用。因此,将其做成静态方法最合适,不需要实例化,能够获取到方法的定义并调用就行。

工具类不实例化的原因是可以节省内存空间,因为工具类提供的是静态方法,通过类就能调用,不需要实例化工具类对象。

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;
 }
}

使用lombok插件,通过注解方式来增强Java代码的编写,在编译期动态生成get和set方法。

import lombok.Data;

@NoArgsConstructor
@Data
@Accessors(chain = true)
public class Person {
    private String name;
    private int age;
}

插件包还提供了@Builder和@Accessors等比较实用的链式编程能力,在一定程度上能提高编码效率。

3.1.4 不可变类

在某些场景下,类为了保证其功能和行为的稳定性和一致性,会被设计为不能被继承和重写的。

定义方式就是在类上面添加final关键字,示例:

public final class String implements Serializable, Comparable<String>, CharSequence {

}

以下是一些不能被继承和重写的类,这在一些底层中间件中会有应用:

java.lang.String
java.lang.Math
java.lang.Boolean
java.lang.Character
java.util.Date
java.sql.Date
java.lang.System
java.lang.ClassLoader
3.1.5 匿名内部类

匿名内部类通常用于简化代码,它的定义和使用通常发生在同一处,它的使用场景如下:

  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();
    }
}
3.1.6 声明类

声明类是Java语言中的基本类型或接口,用于定义类的行为或特性,有的甚至只是个声明,没有具体的方法定义。

• AutoCloseable:表示实现了该接口的类可以被自动关闭,通常用于资源管理。

• Comparable:表示实现了该接口的类可以与其他实现了该接口的对象进行比较。

• Callable:表示实现了该接口的类可以作为参数传递给线程池,并返回结果。

• Cloneable:表示实现了该接口的类可以被克隆。

• Enum:表示实现了该接口的类是一个枚举类型。

• Iterable:表示实现了该接口的类可以迭代。

• Runnable:表示实现了该接口的类可以作为线程运行。

• Serializable:表示实现了该接口的类可以被序列化和反序列化。

• interface:表示实现了该接口的类是一个接口,可以包含方法声明。

• Annotation:表示实现了该接口的类是一个注解,可以用于元数据描述。

3.1.7 Record 类

Record 类在 Java14 中就开始预览,一直到Java17 才正式发布。根据 JEP395 的描述,Record 类是不可变数据的载体,类似于当下广泛应用的各种 model,dto,vo 等 POJO 类,但 record 本身在构造之后不再可赋值。所有的 record 类都继承自 java.lang.Record。Record 类默认提供了全字段的构造器,属性的访问,还有 equals,hashcode,toString 方法,其作用和 lombok 插件非常类似。

定义方式

/**
 * 关键定义的类是不可变类
 * 将所有成员变量通过参数的形式定义
 * 默认会生成全部参数的构造方法
 * @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();

使用场景

通过Record 构建一个临时存储对象,将 Person 数组对象按照年龄排序。

public List<Person> sortPeopleByAge(List<Person> 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;
}
3.1.8 密封类

Java 17推出的新特性密封类(Sealed Classes),主要作用就是限制类的继承。我们知道之前对类继承功能的限制主要有两种:

  1. final修饰类,这样类就无法被继承了;

  2. package-private类,可以控制只能被同一个包下的类继承;

但很显然,这两种限制方式的力度都非常粗,而密封类正是对类继承的更细粒度的控制。

sealed class SealedClass permits SubClass1, SubClass2 {

}

class SubClass1 extends SealedClass {

}

class SubClass2 extends SealedClass {
   
}

在上面的示例中,SealedClass是一个密封类,它包含两个子类SubClass1和SubClass2。在SubClass1和SubClass2的定义中,必须使用extends关键字来继承自SealedClass,并且使用permits关键字来指定它们允许哪些子类来继承。通过使用密封类,可以确保只有符合特定条件的子类才能继承或实现该协议或规范。

3.2 方法定义

3.2.1 构造方法

构造方法是一种特殊的方法,用于创建和初始化对象。构造方法的名称必须与类名相同,并且没有返回类型。在创建对象时,可以通过使用 new 关键字来调用构造方法。

public class MyClass {
    private int myInt;
    private String myString;

    // 构造方法
    public MyClass(int myInt, String myString) {
        this.myInt = myInt;
        this.myString = myString;
    }
}

实现单例模式的一个重要特性就是不允许用户随意创建(new)对象,如何做到安全控制呢?将构造方法声明为私有(private)是必不可少的一步。

3.2.2 方法重写

方法重写是指在子类中重新定义与父类中同名的方法。方法重写允许子类覆盖父类中的方法实现,以便根据子类的需要实现其自己的行为。

class Animal {
    public void makeSound() {
        System.out.println("Animal is making a sound");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myCat = new Cat();
        myCat.makeSound(); // 输出 "Meow"
    }
}

面向对象的三大特性之一的多态,方法重写是其核心。

3.2.3 方法重载

类中定义多个方法,它们具有相同的名称但参数列表不同。方法重载允许我们使用同一个方法名执行不同的操作,根据传递给方法的参数不同来执行不同的代码逻辑。

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); // 输出 5
        System.out.println(result2); // 输出 6.0
    }
}
3.2.4 匿名方法

Java 8 引入了 Lambda 表达式,可以用来实现类似匿名方法的功能。Lambda 表达式是一种匿名函数,可以作为参数传递给方法,或者直接作为一个独立表达式使用。

public static void main(String args[]) {
    List<String> names = Arrays.asList("hello", "world");
    // 使用 Lambda 表达式作为参数传递给 forEach 方法
    names.forEach((String name) -> System.out.println("Name: " + name));

    // 使用 Lambda 表达式作为独立表达式使用
    Predicate<String> nameLengthGreaterThan5 = (String name) -> name.length() > 5;
    boolean isLongName = nameLengthGreaterThan5.test("John");
    System.out.println("Is long name? " + isLongName);
}

3.3 对象定义

3.3.1 单例对象

单例对象是一种可以重复使用的对象,但只有一个实例。它有以下几个作用:

  1. 控制资源的使用:通过线程同步来控制资源的并发访问。

  2. 控制实例产生的数量:达到节约资源的目的。

  3. 作为通信媒介使用:也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。

比如,使用枚举实现单例模式:

public enum Singleton {
 INSTANCE;
 
 public void someMethod() {
 // ...其他代码...
 }
}
3.3.2 不可变对象

Java中的不可变对象是指那些一旦被创建,其状态就不能被修改的对象。不可变对象是一种非常有用的对象,因为它们可以确保对象的状态在任何时候都是一致的,从而避免了因为修改对象状态而引发的问题。实现不可变对象有以下几种方式:

  1. 将对象的状态存储在不可变对象中:String、Integer等就是内置的不可变对象类型;

  2. 将对象的状态存储在final变量中:final变量一旦被赋值就不能被修改;

  3. 将对象的所有属性都设为不可变对象:这样就可以确保整个对象都是不可变的;

一些容器类的操作也有对应的包装类实现容器对象的不可变,比如定义不可变数组对象:

Collections.unmodifiableList(new ArrayList<>());

当领域内的对象作为入参往外传递时,将其定义为不可变对象,这在保持数据一致性方面非常重要,否则对象属性变更的不可预测性,在进行问题定位时,将会非常麻烦。

3.3.3 元组对象

元组(Tuple)是函数式编程语言中的常见概念,元组是一个不可变,并且能够以类型安全的形式保存多个不同类型的对象。它是一种非常有用的数据结构,可以让开发者在处理多个数据元素时更加方便和高效。但原生的Java标准库并没有提供元组的支持,需要我们自己或借助第三方类库来实现。

二元组实现

public class Pair<A,B> {
    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<A,B,C> extends Pair<A,B>{
    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) {
        // 表示姓名,性别,年龄
        Triplet<String,String,Integer> triplet = new Triplet("John","男",18);
        // 获得姓名
        String name = triplet.getFirst();
    }

}

多元组实现

public class Tuple<E> {
    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) {
        // 表示姓名,性别,年龄
        Tuple<String> tuple = new Tuple<>("John", "男", "18");
        // 获得姓名
        String name = tuple.get(0);
    }
}

Tuple主要有以下几个功能:

1. 存储多个数据元素:Tuple可以存储多个不同类型的数据元素,这些元素可以是基本类型、对象类型、数组等;

2. 简化代码:Tuple可以使代码更加简洁,减少重复代码的编写。通过Tuple,我们可以将多个变量打包成一个对象,从而减少了代码量;

3. 提高代码可读性:Tuple可以提高代码的可读性。通过Tuple,我们可以将多个变量打包成一个对象,从而使代码更加易读;

4. 支持函数返回多个值:Tuple可以支持函数返回多个值。在Java中,函数只能返回一个值,但是通过Tuple,我们可以将多个值打包成一个对象返回;

除了自定义之外,实现了元组概念的第三方类库有:Google Guava,Apache Commons Lang,JCTools,Vavr等。

Google Guava库的Tuple提供了更多的功能,并且被广泛使用。比如,为了使元组的含义更加明确,Guava提供了命名元组(NamedTuple)的概念。通过给元组命名,可以更清晰地表示每个元素的意义。示例:

NamedTuple namedTuple = Tuples.named("person", "name", "age");
3.3.4 临时对象

临时对象是指在程序执行过程中临时需要,但生命周期较短的对象。这些对象通常只在使用过程中短暂存在,不需要长期存储或重复使用。

关于临时对象的优化建议如下:

  1. 尽量重用对象。由于系统不仅要花时间生成对象,以后可能还需花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响,重用对象的策略有缓存对象,也可以针对具体场景进行定向优化,比如使用StringBuffer代替字符串拼接的方式;

  2. 尽量使用局部变量。调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快。其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢;

  3. 分代收集。分代垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率;

3.3.5 Valhalla

Java作为高级语言,和更为底层的C语言,汇编语言在性能方面一直存在着不小的差距。为了弥补这一差距,Valhalla 项目于 2014 年启动,目标是为基于 JVM 的语言带来更灵活的扁平化数据类型。

我们都知道Java支持原生类型和引用类型两种。原生数据类型按值传递,赋值和函数传参都会把值给复制一份,复制之后两份之间就再无关联; 引用类型无论什么情况传的都是指针,修改指针指向的内容会影响到所有的引用。而Valhalla又引入了值类型(value types),一种介于原生类型和引用类型之间的概念。

由于应用程序中的大多数Java数据结构都是对象,因此我们可以将Java视为指针密集型语言。这种基于指针的对象实现用于启用对象标识,对象标识本身用于语言特性,如多态性、可变性和锁定。默认情况下,这些特性适用于每个对象,无论它们是否真的需要。这就是值类型(value types)发挥作用的地方。

值类型(value types)的概念是表示纯数据聚合,这会删除常规对象的功能。因此,我们有纯数据,没有身份。当然,这意味着我们也失去了使用对象标识可以实现的功能。由于我们不再有对象标识,我们可以放弃指针,改变值类型的一般内存布局。让我们来比较一下对象引用和值类型内存布局。

去掉了对象头信息,在64位操作系统中值类型节约了对象头16个字节的空间。同时,也意味着放弃对象唯一身份(Identity)和初始化安全性,之前的wait(),notify(),synchronized(obj),System.identityHashCode(obj)等关键字或方法都将失效,无法使用。

Valhalla 在提高性能和减少泄漏的抽象方面将会显著提高:

• 性能增强通过展平对象图和移除间接来解决。这将获得更高效的内存布局和更少的分配和垃圾回收。

• 当用作泛型类型时,原语和对象具有更相似的行为,这是更好的抽象。

截止到2023年9月,Valhalla 项目仍在进行中,还没有正式版本的发布,这一创新项目值得期待的。

四 总结

本文总结了软件开发过程中经常用到的基础常识,分为基础篇和实践篇两个篇章,其中基础篇中着重讲述了类,方法,变量的命名规范以及代码注释好坏的评判标准。实践篇中从类,方法以及对象三个层面分析了常见的技术概念和落地实践,希望这些常识能够为读者带来一些思考和帮助。

作者:京东零售 刘慧卿

来源:京东云开发者社区 转载请注明来源

相关推荐

业务复杂度治理方法论--十年系统设计经验总结

软件设计的核心在于降低复杂性。--《软件设计的哲学》业界对于复杂度并没有统一的定义,斯坦福教授John Ousterhout从认知负担和工作量方面给出了一个复杂度量公式子模块的复杂度cp乘以该模块对应的开发时间权重值tp,累加后得到系统的整体复杂度C这里的子模块复杂度cp是一个经验值需要注意:如果一个子系统特别复杂,但是很少使用及修改,也不会对整体复杂度造成太大影响。例:spring框架内部代码较为复杂,但由于几乎不需要我们去变动,所以对系统的整体复杂度影响并不大。

JDDTechTalk的博客 2444

【转载】golang内存分配

在 1.10 以前 go 的堆地址空间是线性连续扩展的, 比如在 1.10(linux amd64)中, 最大可扩展到 512GB. 因为 go 在 gc 的时候会根据拿到的指针地址来判断是否位于 go 的 heap 的, 以及找到其对应的 span, 其判断机制需要 gc heap 是连续的. 但是连续扩展有个问题, cgo 中的代码(尤其是 32 位系统上)可能会占用未来会用于 go heap 的内存. 这样在扩展 go heap 时, mmap 出现不连续的地址, 导致运行时 throw.

JDDTechTalk的博客 1485

自增主键去哪了?---一次开发过程中的思考

MySQL是作为大家都经常接触的DB,相信大家都会有一定的认知,自增主键不连续大家肯定也遇到过,这次在联调过程中遇到这个情况,在跟别的小伙伴分享的时候,突然就想写一篇文章,文章里面如果有不正确或者不准确的地方也欢迎大家斧正~说实话,作为一名交易端的研发人员,业务开发任务压力真的蛮大的,我也迷茫过,如何在这个过程中成长,珍惜我们遇到的问题,将遇到的问题记录在册,深追问题,你会发现有很多问题其实真的是因为我们某些知识的薄弱点造成的。

JDDTechTalk的博客 1679

万字长文浅谈三高系统建设方法论和实践

整个软件的发展历程是一部软件复杂性对抗史,软件的复杂性分为技术复杂性和业务复杂性,业务复杂性主要是建模和抽象设计,技术复杂性主要是三高(高性能,高并发,高可用)的应对,C端的业务一般以技术复杂性为主,业务复杂性为辅,而B端或者M端的业务通常以业务复杂性为主,技术复杂性为辅。本篇文章主要是从后端研发的视角结合自己多年的B C端系统建设实践谈下三高系统的建设方法论和实践,希望和大家相互交流,共同进步,同时这是我参与创作者计划的第1篇文章。

JDDTechTalk的博客 2527

Vision Pro开发实践(结合24黑马idea)

利用ModelEntity加载模型,添加到RealityView的content中展示,这样可以对模型做各种操作,比如添加子模型,修改光照,材质等要素。举个例子来讲,我需要在运动鞋模型“sneaker”上面添加一个可以与用户交互的白色圆点模型,用于在用户点击的时候添加/展示对应部分的评价。这次结合的idea是我参加黑马比赛时想到的,主要内容是:在商品评价时增加商品的3D模型,用户可以在虚拟现实环境中查看商品的3D模型,并对模型的特定位置进行评论。:通过3D模型动画,用户可以直观地了解产品的特性和使用方法。

JDDTechTalk的博客 2174

买药秒送 JADE动态线程池实践及原理浅析

买药秒送是健康即时零售业务新的核心流量场域,面对京东首页高流量曝光,我们对频道页整个技术架构方案进行升级,保障接口高性能、系统高可用。动态线程池是买药频道应用的技术之一,我们通过3轮高保真压测最终初步确定了线程池的核心参数。但我们仍面临一些保障系统稳定性问题:如何监控线程池运行状态?以及因流量飙升出现任务堆积和拒绝时能否实时报警,线程池核心参数能否做到不重启应用,动态调整即时生效?经调研,业界成熟的动态线程池开源项目有dynamic-tp和hippo4j。

JDDTechTalk的博客 2051

从原理聊JVM(一):染色标记和垃圾回收算法

本篇介绍了JVM中垃圾回收器相关的基础知识,后续会深入介绍CMS、G1、ZGC等不同垃圾收集器的运作流程和原理,欢迎关注。。

JDDTechTalk的博客 1913

BFF层聚合查询服务异步改造及治理实践

本篇主要是思考如何依赖现有框架、环境的能力,从代码层面系统化的实现相关治理规范。最后仍引用王晓老师文章结尾来结束。

JDDTechTalk的博客 1922

QPS提升10倍的sql优化

总结本次优化经历•慢sql 往往是影响数据库性能的大瓶颈,sql写好了不但可以优化性能,还能节约机器成本,降本增效。•最好能看到sql语句执行的第一现场,本次主要是由于查看代码时没有及时注意到索引字段的传参类型不对这一细节,造成花了很多时间分析问题•虽然整个问题分析过程比较曲折,但问题分析的方向应该还是对的,过程中学到不少知识。•表结构的设计也有一些历史遗留原因,site_id 字段在表中定义为整型可能比较符合业务含义。表字段定义和业务含义一致,写sql也不容易犯错。

JDDTechTalk的博客 1793

一站式统一返回值封装、异常处理、异常错误码解决方案—最强的Sping Boot接口优雅响应处理器

Graceful Response内置了两种风格的响应格式,可以在文件中通过进行配置。•gr.responseStyle=0,或者不配置(默认情况)"msg": "有内鬼,终止交易"},"data": {•自定义响应格式如果以上两种格式均不能满足业务需要,可以通过自定义去满足,Response@Override@Override@Override@Overridereturn msg;注意,不需要返回的属性可以返回null或者加上注解。

JDDTechTalk的博客 2006

【原创】【深入浅出系列】之代码可读性

原创声明:该文章是个人在项目中亲历后的经验总结和分享,如有搬运需求请注明出处。这是“深入浅出系列”文章的第一篇,主要记录和分享程序设计的一些思想和方法论,如果读者觉得所有受用,还请“一键三连”,这是对我最大的鼓励。一句话:见名知其义。有人说好的代码必然有清晰完整的注释,我不否认;也有人说代码即注释,是代码简洁之道的最高境界,我也不否认。但我都不完全接受,如果照搬前者,有人会在每个方法、每个循环、每个判断都添加大量注释,对于一个表达不严谨的coder来说,代码与汉字可能词不达意;

JDDTechTalk的博客 1747

动态化-鸿蒙跨端方案介绍

动态化目前已经具备一份代码在iOS、Android、H5三端运行的能力,该方案就是将动态化一码多端能力对接到鸿蒙系统,降低首次适配研发投入;一次开发,四端共用,长期降低业务迭代成本;并且使业务具备随时发布能力。。

JDDTechTalk的博客 2627

【ChatGPT应用篇-1】助力Beauty代码的初体验

以下是一个与ChatGPT 结对编程的一个Excel数据解析案例,大家可以根据我的思考过程进行体验,以下放了一些截图,ChatGPT的反应速度惊人,代码版本一步步进行迭代优化,感触较深的是。对于ChatGPT输出的代码没有做仔细的Review及调试,但是看代码风格基本上是改改就能用于生产的,对于代码规范、命名还是很认可的!10.提升程序效率,把解析性能在提升10倍 (架构师的成本意识,这个问题的回答有点意思)6.替换掉 poi 采用EasyExcel (替换原始的默认技术选型,替换三方包)

JDDTechTalk的博客 1524

一文了解电商大促系统的高可用保障思路-献给技术伙伴们

电商大促是电商平台组织的一种大型销售推广活动,目的是通过提供各种优惠、折扣等方法,提高商品销售额和网站流量,增加消费者的购物欲望,以实现销售目标。电商大促活动通常会在一些特定的节点或者节日举行,比如“双11”、“618”、“黑色星期五”等,这些时期的电商大促极具吸引力,既有大量的商品打折优惠,又有丰富多样的活动供消费者参与,是电商平台提升销售业绩的重要手段。

JDDTechTalk的博客 2122

一起单测引起的项目加载失败惨案

是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。最近在开发一个功能模块时,在功能自测阶段,通过使用单测测试功能的完整性,在测试单测联通性使用到静态方法测试时,发现单测报错,通过查阅解决方案发现需要对。建议大家在构建Maven项目时,仔细检查POM文件中的依赖,确保没有漏掉任何必要的库,以免因为遗漏而引起不必要的问题。但是,在我们的POM文件中,虽然我们添加了对Spring框架的依赖,但是并没有添加。: 返回修改后的字节码。

JDDTechTalk的博客 1909

京东小程序折叠屏适配探索

应用在折叠屏运行时,可以从一个屏幕切换到另一个屏幕。应用应该做好配置变更的适配和界面状态的保存,以保证应用当前任务能无缝迁移到转换后的屏幕,从而为用户提供出色的连续性体验。折叠屏作为未来Android屏幕发展的新趋势,具有很大的发展前景,做好折叠屏相关适配支持也势在必行。小程序相关适配已经跟随京东主站、小家App、小家三星预装版等发布上线,本文是作者进行相关适配的一些心得体会,如有不足敬请见谅,欢迎交流探讨。

JDDTechTalk的博客 1万+

KubeCon China 2024全球大会在香港举行,京东云受邀参加探讨云原生、开源及 AI

和数字化大潮一样,在AI化的革命下,云端也在全面拥抱AI,并在方方面面变得更安全、更高效,让全球各行各业受益。大会首日吸引了来自全球 30 多个国家和地区的 1000 多名参会者,共同探讨云原生、开源及 AI 领域的前沿进展、核心技术和最佳实践。在京东云中,联邦Serverless服务基于联邦管理模型和Serverless应用模型,为联邦应用容器部署、弹性扩展和故障迁移提供JDOS应用容器控制服务。弹性扩缩容技术方案有很广大的应用前景,京东云内部也在进行大量的生产实践和验证。1. 多集群管理和分发实践。

JDDTechTalk的博客 1168

京东搜索重排:基于互信息的用户偏好导向模型

。

JDDTechTalk的博客 1939

探索AC自动机:多关键词搜索的原理与应用案例

目前,大多数自由文本搜索技术采用类似于Lucene的策略,通过解析搜索文本为各个组成部分来定位关键词。这种方法在处理少量关键词时表现良好。但当搜索的关键词数量达到10万个或更多时,这种方法的效率会显著下降,尤其是在需要与词典进行详尽对比的场景中。本文将介绍的Aho-Corasick(AC)自动机作为多模式匹配中的经典算法,不仅能够处理大规模文本数据,还能确保搜索过程的实时性和准确性。AC自动机可以被形象地比喻为一个超级找词机器。

JDDTechTalk的博客 1491

“前端”工匠系列(一):合格的工匠,究竟该搞什么

有效的交流是需要以有效的信息为载体的。在用户端日志查询、特殊边界场景复现、日志排查定位故障等场景,“实时”就不是必要的,这种场景下一般采用T+1查询,但是又引入了大量级日志的存储周期的话题,一般企业应用级的用户日志保存14天就完全够用了,因为对于C端日志来说,更多的是对现场故障的复现、处理及跟进,如果算法策略对用户日志有需要,只需要在一定时间内采用处理任务对用户日志进行一次处理,把输出的标签、行为特征等关键数据存储就可以,基础的用户日志还是应该被存档或清除释放资源供给更有价值的最新日志来使用。

JDDTechTalk的博客 1044

mysql磁盘碎片整理

不管是使用云还是商城数据库,只要使用mysql,必然会遇到Mysql碎片问题痛点,数据量大的业务表应该设计成分区表方便磁盘碎片整理,降低维护成本和业务影响。碎片清理前后,IO性能会上升,SQL执行效率更快。所以,在日常运维工作中,应对碎片进行定期清理,保证数据库有稳定的性能和充足的空间。扩展提到提高IO性能,在紧急情况下还可以考虑开启刷盘(设置),但开启刷盘会有数据丢失风险(集团数据库模板配置参数默认。

JDDTechTalk的博客 1105

【我在京东做产研】校招 2 年,个人角度(成长)回顾 - 行且知

回顾毕业后的工作历程(文章会以现实时间为顺序来进行回顾),有很多大佬的帮助和指点。因此,想将自己的思考也分享给后来的同学们文章从个人角度(成长)出发,回顾工作点滴。后续会从团队角度(互惠)出发,写成长历程二,敬请期待1.JD的技术体系,各种中间件的了解和使用▪各种中间件的使用文档,还是非常有价值的,各种各样的名词,使用细节。如果你都不知道,怎么细节性学习?▪切勿眼高手低:▪程序员都知道文档易读,但是不如手敲一遍。敲完运行运行,可能你就有新的疑问。(例如:JSF 配置后,如何生成代理对象的呀)

JDDTechTalk的博客 927

京东云《黑神话 · 悟空》现已支持快速部署!

黑神话:悟空》是一款以中国神话为背景的动作角色扮演游戏。本服务内置《黑神话:悟空》游戏、《Steam》客户端和远程桌面《GameViewer》软件,配备顶级游戏显卡,通过计算工厂快速部署后按下方文档操作即可开启流畅游戏体验。

JDDTechTalk的博客 1429

京东平台内容合规的技术与挑战

合规域在电商平台中扮演着重要的角色,它相当于电商体系中的“免疫系统”,通过识别恶意的商品、商家以及UGC内容,确保电商生态的健康、有序和可持续发展。因此我们部门致力于打造业内领先的内容理解与审核平台,在具体工作中,通过识别并治理商家在产品、价格、服务维度的违规行为以及短视频、直播、评价、问答等内容的违规信息,降低审核成本、提升运营效率,并最终保障用户体验。。

JDDTechTalk的博客 1102

视频审核架构实践

针对不同的编码器、封装协议、传输协议,提供统一的音视频处理接口。跨平台,兼容200多种编码、180多种封装格式、20多种传输协议。世界上90%以上的音视频开发基于FFmpeg。。

JDDTechTalk的博客 1605

QUIC在京东直播的应用与实践

国内的互联网直播技术从2005年前后兴起,彼时最具代表性的直播产品是由PPLive创始人姚欣在华中科技大学就读期间发起的校园直播项目PPLive。当时的直播技术用的还是基于windows系统自带的mediaplayer内置的COM组件开发的播放器,采用的是RTSP协议。受当时的互联网传输带宽及成本限制,PPLive并没有采用现在比较流行的单播技术,而是采用P2P技术分发直播流。国内的直播技术也进入了一段以P2P技术为主的时期,其中比较有代表性的就是PPLive和PPStream。

JDDTechTalk的博客 1409

突破传统监测模式:业务状态监控HM的新思路

以上是hm的全部技术细节。hm灵魂是数据计算、治理、数据统计。hm根基是集成百家之长。是我们平台研发部自研的一款面向业务的,异常监控处理的解决方案。汇集了团队每位伙伴的智慧。

JDDTechTalk的博客 1247

如何优雅的处理异常

Java 语言按照错误严重性,从 throwale 根类衍生出 Error 和 Exception 两大派系。Error(错误):程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。常见的错误有内存溢出,jvm 虚拟机自身的非正常运行,calss 文件没有主方法。程序本生是不能处理错误的,只能依靠外界干预。Error 是系统内部的错误,由 jvm 抛出,交给系统来处理。程序正常运行中,可以预料的意外情况。比如数据库连接中断,空指针,数组下标越界。

JDDTechTalk的博客 1193

鸿蒙跨端实践-ArkTS和CAPI的混合开发实现

针对以上问题,通过和华为沟通,鸿蒙系统提供了C语言的命令式接口。C组件接口是介于UI组件的Native实现和ArkTS对接层之间的一层C接口封装,它绕过了状态管理对组件变化、刷新的自动化管理,同时避免了JS引擎和C++之间类型转换和跨语言调用的开销,因此具有较好的性能。通过C接口的对接,UI层级能直接和另外两端基本一致,通讯过程直接从JS到C++,C++可以直接调用C接口,流程大大缩短,数据类型转换变少了,列表渲染过程也由接入方自主控制,并且可以做预渲染等优化方案,同时避免了系统的二次布局。。

JDDTechTalk的博客 1757

Proxyless的多活流量和微服务治理

Joylive Agent 是一个基于字节码增强的框架,专注于多活和单元化场景下的流量治理。它提供了以下功能:多活流量调度、全链路灰度发布、QPS和并发限制、标签路由、负载均衡,熔断降级,鉴权等流量治理策略。其特性包括微内核架构、强类隔离、业务零侵入等,使其在保持高性能的同时对业务代码影响最小,是面向Java领域的新一代Proxyless Service Mesh探索实现。重要的事情说三遍:求Star,求Star,求Star。请Star完毕后继续阅读后文。

JDDTechTalk的博客 1823
上一篇: 黄金眼PAAS化数据服务DIFF测试工具的建设实践 | 京东云技术团队
下一篇: 浅析“代码可视化” | 京东云技术团队
京东云技术团队
京东云技术团队 企业官方账号 企业官方账号
博客等级 码龄6年 3626粉丝 686原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值