Java开发手册阅读笔记

编程知识 更新时间:2023-04-26 08:37:25

《Java开发手册》阅读理解,抛砖引玉

前言

最近开发一个项目,由于是第一次搞开发没什么经验,由于在编码上没有什么约束,完全依靠idea的自动提示,在后期重构和开发时带来了一些麻烦,而且代码极其冗余,偶然了解到阿里的这本简约而不简单的《java开发手册》,涉及到了各个方面,并且本土语言阅读也比较流畅,拜读记录一下,在下一个阶段中严格遵循。 这里仅仅记录自己筛选的一些比较帮助大的而自己没有什么意识的条目,并自主添加了一些来源于网络搜索的注释。
注意:由于自己的开发经验过少,本笔记仅仅对 自己没有遵循 的【强制】和对 自己 本阶段有意义的【推荐】和少部分有感悟的【参考】进行筛选、记录、整理、解读。当然由于自己内功不够,大多数解读还是参考网络。
之后开发项目经验有一定积累后再读手册肯定会有新的感悟。

一、编程规约

(一) 命名风格

  1. 【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO /PO / UID 等。

    名词的解释可以看这篇文章: 一篇文章讲清楚VO,BO,PO,DO,DTO的区别,其中的一张图是十分形象的

  2. 【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾。

    这里记录一下接口和抽象类的部分区别,为了简便有些条目只说一方面,另一个反之,以下条目皆基于JDK7,部分新特性用删除线和斜体标注。

    1. 抽象类可以有构造方法,接口中不能有构造方法。
    2. 抽象类中可以有普通成员变量,接口中没有普通成员变量
    3. 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。(在JDK8中接口可以有默认方法)
    4. 接口中的抽象方法只能是public类型的(JDK9新特性可以私有)
    5. 接口中不能包含静态方法 (JKD8新特性接口可以有静态方法)
    6. 抽象类和接口中都可以包含静态成员变量,但接口中定义的变量只能是public static final类型
    7. 一个类可以实现多个接口,但只能继承一个抽象类。

    注意:Java 7,接口可以包含:常量、抽象方法;Java 8,额外包含:默认方法、静态方法;Java 9,私有方法

    接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用。模板方法设计模式是抽象类的一个典型应用,假设某个项目的所有Servlet类都要用相同的方式进行权限判断、记录访问日志和处理异常,那么就可以定义一个抽象的基类,让所有的Servlet都继承这个抽象基类,在抽象基类的service方法中完成权限判断、记录访问日志和处理异常的代码,在各个子类中只是完成各自的业务逻辑代码。

  3. 【强制】类型与中括号紧挨相连来表示数组。
    正例:定义整形数组 int[] arrayDemo。
    反例:在 main 参数中,使用 String args[]来定义。

    要用标准的Java式数组定义 int[] arr;

  4. 【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。
    说明:在本文 MySQL 规约中的建表约定第一条,表达是与否的变量采用 is_xxx 的命名方式,所以,需要
    在设置从 is_xxx 到 xxx 的映射关系。
    反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

  5. 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
    正例:应用工具类包名为 com.alibaba.ei.kunlun.aap.util、类名为 MessageUtils(此规则参考 spring 的框架结构)

    关于项目、包、类的非基础命名的规则推荐这篇博客:Java开发都需要参考的一份命名规范,其中最后的注释规范非常值得新手一看。

  6. 【强制】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。
    说明:子类、父类成员变量名相同,即使是 public 类型的变量也能够通过编译,另外,局部变量在同一方法内的不同代码块中同名也是合法的,这些情况都要避免。对于非 setter/getter 的参数名称也要避免与成员变量名称相同。
    反例:

    public class ConfusingName {
        public int stock;
        // 非 setter/getter 的参数名称,不允许与本类成员变量同名
        public void get(String alibaba) {
            if (condition) {
                final int money = 666;
                // ...
            }
            for (int i = 0; i < 10; i++) {
                // 在同一方法体中,不允许与其它代码块中的 money 命名相同
                final int money = 15978;
                // ...
            }
        }
    }
    class Son extends ConfusingName {
        // 不允许与父类的成员变量名称相同
        public int stock;
    }
    

    一道典型的科班考试题,但是在开发中是严令禁止的。

  7. 【推荐】在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。
    正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
    反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD

    平时自己可能不会用nameList,而是names。(其实是idea代码提示让我这么做的

  8. 【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。
    正例:接口方法签名 void commit(); 接口基础常量 String COMPANY = “alibaba”;
    反例:接口方法定义 public abstract void f();
    说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。

  9. 接口和实现类的命名有两套规则:
    1)【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。
    正例:CacheServiceImpl 实现 CacheService 接口。
    2)【推荐】如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词)。
    正例:AbstractTranslator 实现 Translatable 接口。

    虽然一直这么写,今天才知道是SOA理念。这里推荐一个知乎回答下面找 虫师 的回答是比较符合java描述的,如何通俗易懂地解释什么是SOA?,其中说到的web serveice定义:
    【Web Service是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,可使用开放的XML标准来描述、发布、发现、协调和配置这些应用程序,用于开发分布式的互操作的应用程序。】
    个人感觉SOA和IOC思想有小部分重合之处,依靠xml作为粘合剂,把各个模块(类)极大的解耦,就像U盘和电脑一样互不干扰、互不依赖只是互相或单方面加载,使开发者只注重自己的业务。

  10. 【参考】各层命名规约:
    A) Service/DAO 层方法命名规约
    1) 获取单个对象的方法用 get 做前缀。
    2) 获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects。
    3) 获取统计值的方法用 count 做前缀。
    4) 插入的方法用 save/insert 做前缀。
    5) 删除的方法用 remove/delete 做前缀。
    6) 修改的方法用 update 做前缀。
    B) 领域模型命名规约
    1) 数据对象:xxxDO,xxx 即为数据表名。
    2) 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
    3) 展示对象:xxxVO,xxx 一般为网页名称。
    4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。

    虽属于参考等级,单个人认为很有必要。

(二) 常量定义

  1. 【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
    反例:
    // 本例中,开发者 A 定义了缓存的 key,然后开发者 B 使用缓存时少了下划线,即 key 是"Id#taobao"+tradeId,导致出现故障

    String key = "Id#taobao_" + tradeId;
    cache.put(key, value);
    

    自己竟是高度魔法值的使用者,魔法值影响了代码的可读性,今后要尽全力改正。
    推荐一篇知乎问答:在阿里巴巴java开发手册中,为什么不允许出现任何魔法值(即未经定义的常量)直接出现在代码中?
    例如在开发中"yes"要定义为

    public static final String YES = "yes";
    

    在这片问答中也讨论了 如响应码 200,和 yes 此类的国际通用标志是否需要严格遵循此条目,个人认为这类常量无论是否遵循,在项目开发时统一约定好就行。

  2. 【强制】在 long 或者 Long 赋值时,数值后使用大写字母 L,不能是小写字母 l,小写容易跟数字混淆,造成误解。
    说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2?

  3. 【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
    说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解,也不利于维护。
    正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 SystemConfigConsts 下。

    目前项目太小用不到太多的常量,之后肯定会用到的 mark一下

  4. 【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。
    1) 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。
    2) 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。
    反例:易懂变量也要统一定义成应用内共享常量,两位工程师在两个类中分别定义了“YES”的变量:

     类 A 中:public static final String YES = "yes";  
     类 B 中:public static final String YES = "y";  
     A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致线上问题。  
    

    3) 子工程内部共享常量:即在当前子工程的 constant 目录下。
    4) 包内共享常量:即在当前包下单独的 constant 目录下。
    5) 类内共享常量:直接在类内部 private static final 定义

  5. 【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义。
    说明:如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。
    正例:

    public enum SeasonEnum {
        SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
        private int seq;
        SeasonEnum(int seq) {
            this.seq = seq;
        }
        public int getSeq() {
            return seq;
        }
    }
    

(三) 代码格式

  1. 【强制】左小括号和右边相邻字符之间不出现空格;右小括号和左边相邻字符之间也不出现空格;而左大括号前需要加空格。详见第 5 条下方正例提示。
    反例:if (空格 a == b 空格)

左大括号前需要加空格,说实话这条我是无意识的, 一直靠的是一件格式化,今天才知道一件格式化里也是遵循这一条的

  1. 【强制】if/for/while/switch/do 等保留字与括号之间都必须加空格。

  2. 【强制】采用 4 个空格缩进,禁止使用 Tab 字符。
    说明:如果使用 Tab 缩进,必须设置 1 个 Tab 为 4 个空格。IDEA 设置 Tab 为 4 个空格时,请勿勾选 Use tab character;而在 Eclipse 中,必须勾选 insert spaces for tabs。
    正例: (涉及原文 1-5 点)

    public static void main(String[] args) {
        // 缩进 4 个空格
        String say = "hello";
        // 运算符的左右必须有一个空格
        int flag = 0;
        // 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
        if (flag == 0) {
            System.out.println(say);
        }
        // 左大括号前加空格且不换行;左大括号后换行
        if (flag == 1) {
            System.out.println("world");
            // 右大括号前换行,右大括号后有 else,不用换行
        } else {
            System.out.println("ok");
            // 在右大括号后直接结束,则必须换行
        }
    }
    

    如果使用Tab缩进确实会导致格式问题,我遇到过的有在 SQLyog 里写 sql 时,用 vscode 和其他编辑器打开格式变乱的问题。

  3. 【强制】在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。
    正例:

    double first = 3.2d;
    int second = (int)first + 2;
    
  4. 【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
    1)第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。
    2)运算符与下文一起换行。
    3)方法调用的点符号与下文一起换行。
    4)方法调用中的多个参数需要换行时,在逗号后进行。
    5)在括号前不要换行,见反例。

    正例:

      StringBuilder sb = new StringBuilder();
      // 超过 120 个字符的情况下,换行缩进 4 个空格,并且方法前的点号一起换行
      sb.append("yang").append("hao")...
      		.append("chen")...
      		.append("chen")...
      		.append("chen");
    

    反例:

    StringBuilder sb = new StringBuilder();
    // 超过 120 个字符的情况下,不要在括号前换行
    sb.append("you").append("are")...append
    	("lucky");
    
    // 参数很多的方法调用可能超过 120 个字符,逗号后才是换行处
    method(args1, args2, args3, ...
    	, argsX);
    

    标准的换行,可以提高代码阅读体验

  5. 【强制】IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符使用 Unix 格式,不要
    使用 Windows 格式。

    不知道多少人和我一样,只知道 encoding 设置 utf8 能防乱码,但是对于 Unix 换行符不知所云,这跟不同操作系统下换行符不同有关,Win下是 0x0D0A(CRLF)可以理解为/r/n,而Linux的换行符是 0x0A(LF)即 \n,这可能会对跨平台文件带来麻烦,而且github平台统一使用 \n 作为换行符,只不过在Win下提交代码时会进行自动转换,但是依然可能会出现一些问题。详细了解可以参考GitHub文档,关于如何修改IDEA换行符可以参考IDEA批量替换文件换行符、分隔符CRLF、LF、CR。

  6. 【推荐】单个方法的总行数不超过 80 行。
    说明:除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过80 行。
    正例:代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加清晰;共性逻辑抽取成为共性方法,便于复用和维护。

(四) OOP 规约 (即面向对象规约)

  1. 【强制】所有的覆写方法,必须加@Override 注解。
    说明:getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加@Override 可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。

    不只是覆写接口和抽象类,继承下来的子类重写也要加注解。

  2. 【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。
    说明:可变参数必须放置在参数列表的最后。(建议开发者尽量不用可变参数编程)
    正例:public List listUsers(String type, Long… ids) {…}

    关于可变参数可以参考简书:Java 可变参数

  3. 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。

  4. 【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
    正例:“test”.equals(object);
    反例:object.equals(“test”);
    说明:推荐使用 JDK7 引入的工具类 java.util.Objects#equals(Object a, Object b)

    帮助很大,关于工具类的equals函数可以好好利用。

  5. 【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
    说明:对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

    大坑啊大坑。。。。。。但这里要注意强调了整型包装类,浮点型是不适用的,具体下面几条会提及到

  6. 【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。

这就涉及到计算机原理中浮点数的储存了,计算机内部是用位来存储和处理数据的。浮点数相加实际上会有误差的,例如 0.1 + 0.2 在计算机里并不等于 0.3。所以建议直接采用Long储存或者使用大数类,使用大数类的注意点也在下面几条有提及。详细可以参考【优雅的避坑】你的钱算错了!为什么0.1+0.2不等于0.3了!?

  1. 【强制】浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。
    说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。
    反例:

    float a = 1.0F - 0.9F;
    float b = 0.9F - 0.8F;
    if (a == b) {
    // 预期进入此代码块,执行其它业务逻辑
    // 但事实上 a==b 的结果为 false
    }
    Float x = Float.valueOf(a);
    Float y = Float.valueOf(b);
    if (x.equals(y)) {
    // 预期进入此代码块,执行其它业务逻辑
    // 但事实上 equals 的结果为 false
    }
    

    正例:

    (1) 指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。

    float a = 1.0F - 0.9F;
    float b = 0.9F - 0.8F;
    float diff = 1e-6F;
    if (Math.abs(a - b) < diff) {
    System.out.println("true");
    }
    

    (2) 使用 BigDecimal 来定义值,再进行浮点数的运算操作。

    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("0.9");
    BigDecimal c = new BigDecimal("0.8");
    BigDecimal x = a.subtract(b);
    BigDecimal y = b.subtract(c);
    if (x.compareTo(y) == 0) {
    System.out.println("true");
    }
    
  2. 【强制】如上所示 BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法。
    说明:equals()方法会比较值和精度(1.0 与 1.00 返回结果为 false),而 compareTo()则会忽略精度。

  3. 【强制】禁止使用构造方法 BigDecimal(double)的方式把 double 值转化为 BigDecimal 对象。
    说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
    如:BigDecimal g = new BigDecimal(0.1F); 实际的存储值为:0.10000000149
    正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。

    BigDecimal recommend1 = new BigDecimal("0.1");
    BigDecimal recommend2 = BigDecimal.valueOf(0.1);
    

    以上几条总结了浮点数的一些坑,都是非常值得深思的。

  4. 关于基本数据类型与包装数据类型的使用标准如下:
    1) 【强制】所有的 POJO 类属性必须使用包装数据类型。
    2) 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
    3) 【推荐】所有的局部变量使用基本数据类型。
    说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
    正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
    反例:某业务的交易报表上显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线-。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。

    这里涉及到数据库查询的坑,Dao层查询出来的pojo其实是包装类的类型,如果数据库中没有默认值,那么有可能存在null数据,查询出来将null赋给基本类型会报出空指针异常。
    局部变量使用基本类型可以提高性能。
    但是知乎上也有大神说定制标准把数据库全部设定一个默认值,在业务上使用基本类型来 提高性能 和 运算逻辑安全 的做法。具体的回答可以参考:阿里巴巴java开发手册的一个小疑问,关于基本数据类型与包装数据类型的使用标准?

  5. 【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。
    反例:POJO 类的 createTime 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。

    建议dao的默认值直接在数据库中定义,string 由于JDK源码默认值为"",在判定逻辑时也要注意。

  6. 【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修serialVersionUID 值。
    说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。

    serialVersionUID 用来标识一个序列化对象保证唯一性,如果不指定的话会根据字段自动生成,所以建议手动指定此字段,防止修改类字段后反序列化挂掉。
    其中有一个坑是,Map<String,Object>,在序列化时,传入Long,Json 反序列化 Map 时如果原始值小于 Int 最大值,反序列化后原本为 Long 类型的字段,变为了 Integer 类型(话说真的有人拿map来做返回值吗,我经验浅薄没有见过。。)。 推一个专栏 https://www.jianshu/p/50d4edc7f7f2

  7. 【推荐】setter 方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名。在 getter/setter 方法中,不要增加业务逻辑,增加排查问题的难度。

(五) 日期时间

  1. 【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
    说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY就是下一年。
    正例:表示日期和时间的格式如下所示:
    new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”)

  2. 【强制】不允许在程序任何地方中使用:1)java.sql.Date。 2)java.sql.Time。 3)java.sql.Timestamp。
    说明:第 1 个不记录时间,getHours()抛出异常;第 2 个不记录日期,getYear()抛出异常;第 3 个在构造方法 super((time/1000)*1000),在 Timestamp 属性 fastTime 和 nanos 分别存储秒和纳秒信息。
    n which year(JDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY就是下一年。
    正例:表示日期和时间的格式如下所示:
    new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”)

  3. 【强制】不允许在程序任何地方中使用:1)java.sql.Date。 2)java.sql.Time。 3)java.sql.Timestamp。
    说明:第 1 个不记录时间,getHours()抛出异常;第 2 个不记录日期,getYear()抛出异常;第 3 个在构造方法 super((time/1000)*1000),在 Timestamp 属性 fastTime 和 nanos 分别存储秒和纳秒信息。
    反例: java.util.Date.after(Date)进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG(JDK9 已修复),可能导致比较时的意外结果

更多推荐

Java开发手册阅读笔记

本文发布于:2023-04-19 21:35:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/f269bc0e5874ce7531b1487c5ae1f21d.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:手册   笔记   Java

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 89107文章数
  • 22006阅读数
  • 0评论数