首页 / 资讯中心 / 文章详情

Java基础详解:数据类型、运算符与流程控制的易错点与面试题

Java基础详解:数据类型、运算符与流程控制的易错点与面试题 ★ FEATURED ARTICLE
想写这篇总结其实是因为我最近在帮团队做新人培训时发现一个现象很多能写出Spring Boot接口、能用MyBatis操作数据库的初级开发一问数据类型和运算符却含糊其辞。尤其是i和i的区别、5/2到底等于几、switch不写break会怎样这种基础层面的知识恰恰是线上Bug和面试失分最多的重灾区。所以这篇把数据类型、运算符、流程控制这三块Java地基彻底拆开结合易错点做一次系统梳理不管你是准备Java面试还是刚走完Java学习路线想自查一遍都值得花20分钟认真看完。我尽量用代码和场景说话不绕弯子每一条都按原理 案例 避坑的顺序展开。1. 数据类型八种基本类型的边界与我以为我知道的坑1.1 八种基本类型的取值范围与默认值背下这张表至少少踩一半坑Java的基本数据类型一共八种这是Java开发工程师面试题里出现频率最高的送分题但能完整答对的人其实不多。很多人张口就背byte、short、int、long、float、double、char、boolean可一问到long类型的默认值是多少float范围到底多大就卡壳了。我把这张表完整列出来建议直接存下来类型位数取值范围或表示范围默认值使用场景byte8-128 ~ 1270文件流、网络协议报文、节省内存的小整数short16-32768 ~ 327670很少直接使用多用于协议解析int32-2147483648 ~ 21474836470默认整数类型计数器、索引、状态值long64-9223372036854775808 ~ 92233720368547758070L时间戳、订单号、ID这类大整数float32约 ±3.4E387位有效数字0.0f科学计算、图形坐标double64约 ±1.7E30815位有效数字0.0d默认浮点类型多数小数计算char160 ~ 65535Unicode字符\u0000单个字符、汉字、ASCII码boolean不固定true / falsefalse逻辑判断、开关状态这里有几个关键点值得展开第一整数字面量默认是int。你写long num 3000000000;会直接编译报错因为3000000000已经超出int范围必须写成3000000000L。同样的float f 3.14;也是错的因为3.14默认是double必须加f后缀。这类问题在项目里经常以改配置、改常量的方式出现改完一编译全是红报错就是字面量类型不匹配。第二char其实是无符号整数。它虽然是字符类型但底层是0到65535的整数所以char c A; System.out.println(c 1);输出的是66而不是B。这一点在ASCII码处理、字符串拼接、字符判断场景里非常容易踩坑比如判断字符是否为数字时c 0 c 9就是标准的整数比较写法。第三float和double不是精确小数。它们用二进制科学计数法表示很多十进制小数无法精确存储所以0.1 0.2的结果不是0.3而是0.30000000000000004。做金额计算必须用BigDecimal用浮点数算金额在线上是会出大事的。我见过一个统计报表用double累加了几万条金额最后一位总是差几分钱排查了半天就是浮点精度问题。1.2 基本类型与引用类型的本质差异值传递还是引用传递为什么总被问Java的变量分两类基本类型变量直接存值引用类型变量存的是对象的内存地址。这个差异派生了两个经典问题第一个问题是**Java是值传递还是引用传递**。严格说Java只有值传递。基本类型传的是值本身方法内怎么改都不影响外面引用类型传的是地址的值所以方法内通过这个地址修改对象属性外面能看到变化但如果在方法内重新给参数赋值一个新对象外面的引用不会变。我经常用一段代码说明public static void main(String[] args) { User user new User(张三); change(user); System.out.println(user.getName()); // 输出 李四 int a 10; add(a); System.out.println(a); // 输出 10 } static void change(User u) { u.setName(李四); } static void add(int x) { x x 1; }这个机制在工作中最直接的影响就是代码审阅看到方法内部改了一个入参对象的属性你要立刻意识到调用方也会受影响这是隐式副作用的主要来源。所以现在很多团队的编码规范要求入参对象尽量用只读方式处理或者明确标注会被修改。第二个问题是String的特殊性。String是引用类型但它是不可变对象字符串常量池保证了相同内容的字面量String常量指向同一地址。所以String s abc; s def;并不是在修改原对象而是让s指向了一个新对象原abc字符串依然在常量池里。这也解释了为什么字符串拼接用会产生许多中间对象循环里拼接字符串要用StringBuilder。1.3 包装类型的缓存机制与equals使用规范比较为什么总是翻车基本类型都有对应的包装类Byte、Short、Integer、Long、Float、Double、Character、Boolean。Java为其中的整数类型提供了缓存机制范围是-128到127以及Character的0到127。这意味着下面这段代码的结果非常反直觉Integer a 127; Integer b 127; System.out.println(a b); // true因为从缓存里取同一个对象 Integer c 128; Integer d 128; System.out.println(c d); // false超出缓存范围各自new了新对象 Integer e new Integer(127); System.out.println(a e); // false手工new的不会走缓存从Java 5开始Integer a 127会自动装箱成Integer.valueOf(127)而valueOf方法内部就是先查缓存命中直接返回缓存对象。所以128的时候两个引用指向不同对象自然返回false。这个坑在开发中最典型的翻车场景是从数据库或缓存框架取主键值类型是Integer然后用比较两个ID是否相等结果小的时候没问题数据量一上去超过127就出现明明一样却不相等的诡异Bug。所以凡是包装类型的比较一律用equals或者先拆箱成基本类型int再用。这里还要注意Integer在拆箱过程中如果值是null会抛出NullPointerException这个放在后面的三目运算符部分细说。1.4 类型转换自动提升、强制转换和精度丢失的真相Java的类型转换分隐式和显式两种。隐式转换发生在小范围转大范围时比如int转long、long转float、char转int编译器自动完成。反过来就不行比如long直接赋给int必须显式强转强转就是放弃高位数据程序员明确告知编译器我知道可能有损失但我接受。最容易出错的三个场景场景一复合赋值运算符的隐式强转。看这两行byte b 1; b b 1; // 编译失败b1结果是int隐式转byte是窄化 b 1; // 编译通过复合赋值自带隐式强转编译期b 1因为是int运算所以不允许直接赋给byte但b 1等价于b (byte)(b 1)编译器帮你加了强转。这在面试里非常喜欢考你得能解释为什么第二行不报错。但也要注意b 1编译通过不代表一定安全如果b是127加1后溢出变成了-128这种溢出是静默发生的Java不会抛任何异常。所以复合赋值是编译安全但逻辑不安全的典型。场景二多个类型混合运算时的自动提升。规则是byte、short、char参与运算时先提升为int如果表达式里有long整体提升为long有float则整体为float有double则整体为double。所以char c A; int i c 1;得到665 / 2得到2而5.0 / 2得到2.53L 1.0f的结果是float而不是long。场景三浮点转整数是直接截断而不是四舍五入。(int) 9.9结果是9不会变成10。而且浮点转int如果超出int范围结果是int的最大值或最小值不会抛异常。比如double d 3.9E20; int i (int) d; System.out.println(i); // 输出 2147483647这种静默的边界值处理很容易被忽视在数据上报、协议解析、外部系统对接的字段转换里稍不注意就会用错误数据污染业务表。提示任何时候做数据类型强制转换请先判断源值是否在目标范围内不要依赖Java的静默行为。2. 运算符每个符号背后都藏着一个面试题2.1 自增自减的求值时机i和i到底谁先算i和i表面上是先使用后自增和先自增后使用但真正理解需要深入到赋值过程。看这个经典题int i 0; i i; System.out.println(i); // 结果是 0为什么不是1因为i这个表达式求值后的值是自增前的0然后赋值操作把0赋给了i覆盖了i自增后的1。整个过程是读取i的当前值0作为i表达式的返回值i自增变成1把步骤1的返回值0赋给i。所以最终i还是0。对应地i i;结果是1因为i表达式的值是自增后的1。这类题在Java开发工程师面试题里几乎是必出的而且经常和循环、数组遍历组合出现。更复杂的版本是混合运算int a 3; int b a a a;拆解过程第一个a取3a变成4第二个a先加变成5再取5第三个a取5a变成6。所以b 3 5 5 13a最终是6。我的建议是日常写代码时尽量把自增自减单独成行不要嵌进复杂的表达式里。一方面可读性差另一方面求值顺序一乱就是隐藏Bug。如果面试遇到就用表达式的返回值和变量自增后的值分开来推百试不爽。2.2 整数除法、取余规则与除零异常为什么5/2不等于2.5int / int永远是int结果直接舍弃小数部分。5 / 2是2-5 / 2是-2注意这里靠近0的方向截断不是向下取整。所以-5 / 2不等于-3这一点在做分页、批次分割的计算时特别容易出问题。取余运算符%的规则是结果的符号与被除数一致。System.out.println(5 % 2); // 1 System.out.println(-5 % 2); // -1 System.out.println(5 % -2); // 1这在判断奇偶时会引入一个易错点如果用x % 2 1判断奇数负数就会判断失败因为-5 % 2等于-1。正确写法是x % 2 ! 0或者用位运算(x 1) ! 0。我的项目代码里做奇偶判断一律用(x 1) 0既快又不会出符号问题。除零问题也要分情况。整数除以0会抛ArithmeticException这是运行时异常int x 10 / 0; // 抛出 java.lang.ArithmeticException: / by zero但浮点数除以0不会抛异常得到的是Infinity或NaNdouble d 10.0 / 0; // Infinity double nan 0.0 / 0; // NaN这个差异在报表计算、指标统计里影响很大。如果某条数据的分母是0整数除法直接造成请求失败而浮点除法会返回一个看起来正常的无穷大最后展示给用户的可能是乱码数字。所以我通常建议任何涉及除法的计算先判分母是否为零而且要用Double.isFinite(result)这类方法校验结果。2.3 逻辑运算符的短路求值与、||与|的差别不仅是少写一个字符和||是短路运算符意思是左侧的结果已经能决定整个表达式的值时右侧就不再执行。和|是位运算符如果用在boolean表达式上则不具备短路能力两侧必定执行。经典场景是空指针保护if (str ! null str.startsWith(java)) { // 只有当str不为null时才执行后面的判断 }如果把换成str为null时右侧的str.startsWith(java)照样执行直接抛NullPointerException。同理if (flag || expensiveCheck())里如果flag已经是trueexpensiveCheck()根本不会执行这在性能上能省不少事。但短路带来一个隐蔽副作用右侧副作用语句不会执行。比如boolean result (a 0) || (count 10);如果a 0为truecount永远不会执行count保持不变。这种逻辑运算符右侧依赖副作用的写法是代码异味排查线上问题时特别容易让人困惑建议绝对不要这么写。另外提醒一个比较冷门但面试偶尔会出的点和|作用于普通整数时是纯粹的位运算和逻辑运算没有任何关系比如3 5结果是13 | 5结果是7。初学者常常把和混为一谈用错之后行为诡异。2.4 位运算符用途、优先级顺序与经典实战场景位运算符包括与、|或、^异或、~取反、左移、有符号右移、无符号右移。最常见的使用场景有三个第一个是状态位压缩。用一个int同时承载多个开关状态比如权限控制里bit0表示读权限、bit1表示写权限、bit2表示执行权限通过判断是否拥有某权限通过|追加权限。这种方式在配置项多但状态值少时非常节省空间。final int READ 1 0; // 1 final int WRITE 1 1; // 2 final int EXEC 1 2; // 4 int permission READ | WRITE; // 3 boolean canRead (permission READ) ! 0; // true boolean canExec (permission EXEC) ! 0; // false第二个是颜色分量提取。ARGB颜色值存于int用和取出红绿蓝分量这在Android开发、图像处理里很常见int color 0xAABBCCDD; int alpha (color 24) 0xFF; int red (color 16) 0xFF; int green (color 8) 0xFF; int blue color 0xFF;第三个是快速乘除2的幂。x n相当于乘以2的n次方x n相当于除以2的n次方。但注意是向下取整的对于负数就麻烦了-7 1结果是-4因为-7/2取整是-4向下取整而-7 / 2在Java里是-3。所以在通用业务里别轻易用右移替代除法只有明确知道数值非负时才安全。位运算符的优先级是个容易记错的点。高频记忆口诀算术、移位、比较、位、逻辑、赋值。完整顺序是() 括号 ! 一元运算符 * / % - instanceof ! ^ | || ?: 赋值运算符注意优先级高于^^高于|所以a b ^ c先算a b。日常写代码强烈建议用括号把位运算括清楚不要依赖优先级因为同事阅读代码时大概率不会去查优先级表。2.5 三目运算符的类型推断与隐藏的NPE风险三目运算符condition ? expr1 : expr2看起来很简单但类型推断规则会在运行时埋雷。Java会根据expr1和expr2的类型推断整个表达式的类型规则是取两者的父类型且如果有类型交叉就做自动提升。最容易出事的例子Integer a null; int b a ! null ? a : 0;这段代码编译能通过但在a ! null为false时整个表达式走: 0分支b最终等于0不抛NPE。真正的坑在下面这个MapString, Integer map new HashMap(); Integer val map.get(key); int result val ! null ? val : -1;这里val为null时走的是: -1分支因为-1是基本类型int整个三目表达式的类型被推断为int所以null分支的val会被强制拆箱成int结果触发NullPointerException。虽然看起来走了val为null的分支但编译器在计算表达式类型时已经把val那侧按int处理了。更隐蔽的是两个包装类型混合Integer i null; Long l 100L; Object o i ! null ? i : l; // NPE因为类型统一为longexpr1是Integerexpr2是Long两者的公共类型是Long所以表达式的实际类型是longi一旦为null就要拆箱NPE避免不了。这个在写通用工具类时非常容易踩。我的经验判断很简单三目表达式两侧类型不一致时尽量先把值拆箱或统一类型再走三目。或者干脆用if-else可读性还更好。凡是可能为null的Integer、Long参与三目运算默认怀疑会出NPE。3. 流程控制代码走向的决策细节与常见翻车现场3.1 if-else的悬空else与建议写法Java语法规定else总是与最近的未匹配if配对。这就是著名的悬空else问题boolean a false; boolean b true; if (a) if (b) System.out.println(a and b); else System.out.println(else);你以为else匹配第一个if实际它匹配的是第二个if。这个缩进是错的编译器是认最近匹配的不认缩进。结果是a为false时跳过外层if根本不进入内层if所以else也不会执行。逻辑上此代码的else分支永远无法进入因为只有当内层if的b为false时才会到else但b始终为truea为false时外层直接结束更不会执行else。要避免这个问题只靠缩进是不够的必须给if、else都加大括号。Java编码规范里有一条单个if语句即使只有一行也必须写大括号。按这个规范写悬空else基本不会发生因为else前面必然有一个右花括号配对关系一目了然。还有一层进阶技巧if-else嵌套过深时优先反转条件提前return。比如// 反面写法 if (user ! null) { if (user.isActive()) { if (user.getAge() 18) { doSomething(); } } } // 正面写法 if (user null) return; if (!user.isActive()) return; if (user.getAge() 18) return; doSomething();这种卫语句风格能显著降低嵌套深度逻辑链路也更清楚。代码评审时这是我必抠的一项。3.2 switch语句break、fall-through与case常量的那些坑switch的fall-through行为是Java初学者最常踩的坑之一。case分支如果不写break会继续执行下一个case的代码直到遇到break或switch结束。int score 85; switch (score / 10) { case 10: case 9: System.out.println(优秀); break; case 8: System.out.println(良好); break; default: System.out.println(一般); }这里的技巧是故意利用fall-through把多个case合并例如10和9都算优秀。但更多场景是忘记写break导致每个case后面所有代码全部执行。我在线上修过这样的Bug状态机的状态流转switch里漏了break导致一个订单状态连续穿透了好几个分支最后跑到支付成功。排查起来非常痛苦因为逻辑上每一单的状态都对不上。关于switch还有几个硬性规则case后面的值必须是编译期常量不能是运行期变量从Java 7开始支持String类型的switch底层用hashCode和equals实现支持byte、short、char、int及其包装类型还有枚举从Java 14开始支持switch表达式可以省略break用-箭头语法并支持yield返回值。如果项目环境允许我强烈建议用新式箭头语法String type switch (day) { case MON, FRI - 工作日; case SAT, SUN - 休息日; default - 未知; };不用break自动跳出杜绝了fall-through问题还能直接作为表达式赋值代码干净多了。3.3 循环的正确打开方式for的执行顺序、边界条件和删除陷阱for循环的三段执行顺序是初始化只执行一次→ 条件判断 → 循环体 → 更新语句 → 条件判断……所以下面这段的输出顺序是固定的for (int i 0; i 3; i) { System.out.print(i ); } // 输出 0 1 2循环边界还是是最常见的差一错误源头。遍历长度为n的数组标准写法是i n写i n会越界抛ArrayIndexOutOfBoundsException。但要注意有些场景从1开始计数条件就变成i n写错方向就直接少算一次或死循环。while和do-while的选择也有讲究先判断后执行用while至少执行一次用do-while。但do-while在业务代码里用得很少属于冷门但面试会问的语法。我印象里do-while最适合的场景是用户输入校验必须至少跑一遍这类交互流程。循环里最隐蔽的坑是集合遍历时删除元素。for (String s : list)这种增强for循环底层用的是迭代器如果在循环体内直接list.remove(s)会抛出ConcurrentModificationException。安全做法是用IteratorIteratorString it list.iterator(); while (it.hasNext()) { String s it.next(); if (needRemove(s)) { it.remove(); } }或者用Java 8的removeIflist.removeIf(s - needRemove(s));这个坑我见得太多了很多新人写遍历一遍把符合条件的删掉就会翻车。3.4 break、continue、return与标签跳转跳出多层循环的正确姿势break只能退出当前一层循环continue是跳过本次循环进入下一次return是直接结束整个方法。业务里有时需要从双层循环中跳出来常见的误区是只用一个break结果只跳出了内层外层继续跑。标准做法是用标签labelouter: for (int i 0; i 5; i) { for (int j 0; j 5; j) { if (i * j 10) { break outer; // 退出双层循环 } } }标签放在循环之前break outer直接跳出标签指向的外层循环。continue也可以带标签效果是跳到外层循环的下一轮。不过标签跳转可读性一般很多团队禁止在业务代码里使用。如果遇到复杂嵌套我更倾向于把内层逻辑抽成独立方法用return来提前结束既清晰又符合结构化编程的思想。还有一个细节是switch里的break和循环的关系Java的break默认只影响最近的switch或循环不会跨结构。如果在switch里想退出外面的for循环单写break是不行的同样需要标签。提示能不用标签就不标签把逻辑抽方法。标签一旦嵌套多了读代码的人很难快速看出跳哪去了。4. 进阶串联一段真实代码里的综合易错点演练前面三章是分模块拆解但实际项目里的Bug往往是跨模块联动发生的。我构造一段看起来没问题的代码把刚才提到的所有易错点串起来你可以在自己IDE里跑一遍看输出是否和预期一致。public class TestCode { public static void main(String[] args) { int sum 0; for (int i 0; i 5; i) { sum i; } System.out.println(sum sum); float f 3.14f; long l 100L; int i2 (int) l; System.out.println(i2 i2); char ch A; System.out.println(ch1 (ch 1)); int a 5; int b 2; System.out.println(5/2 (a / b)); System.out.println(-5%2 (-5 % 2)); int x 0; if (x ! 0 10 / x 1) { System.out.println(safe); } else { System.out.println(no divide); } Integer val null; int result val ! null ? val : -1; System.out.println(result result); int num 10; switch (num) { case 10: System.out.println(ten); case 20: System.out.println(twenty); default: System.out.println(default); } } }现在逐段分析第一段循环i从0开始每次循环体内执行sum i即sum加上自增前的i值然后i再自增1最后for的更新语句i又自增一次。所以实际每次循环i跳跃2步0、2、4。sum依次加的是0、2、4最终sum6循环结束后i6不满足条件退出。很多人以为sum是0加到4等于10其实差远了。第二段类型转换(int) 100L等于100没问题。但如果long值大于int范围结果会截断成int极值这一点要警惕。第三段ch 1char参与运算自动提升为int结果是66不是B。第四段5/22整数除法截断-5%2-1符号跟随被除数。第五段短路逻辑x是0x ! 0为false右侧10 / x不会执行不会抛除零异常。这是的典型用法。第六段三目运算val是null三目表达式两侧类型分别是Integer和int整个表达式类型推断为intval为空时拆箱失败直接抛NullPointerException。很多人跑到这里还一脸懵明明走了: -1分支为什么会NPE这就是前面2.5节说的隐藏坑。第七段switchnum是10匹配case 10但没写break会继续执行case 20和default的输出所以打印三行ten、twenty、default。如果把这段代码丢给一个小白去调他可能只看到结果不对却不知道是六七个独立问题叠加造成的。这正是我把三类知识放一起的原因排查Bug时不能孤立地看一个语法点要能从表达式的类型系统、运算符求值顺序、流程控制穿透三个角度同时分析。4.1 用IDE调试器逐行验证中间值遇到这种综合题不要用System.out.println瞎打直接把断点打在变量上用IDE的调试器看每一行的中间值。我调试时最习惯看的是表达式求值窗口把i、val ! null ? val : -1这类复杂表达式粘贴进去IDE会显示当前的求值结果和类型比人脑推演快得多。还有一个非常实用的工具在Java里可以用javap -c查看字节码看i在操作数栈上的真实过程。虽然日常开发用不上但面试前用这个方法理解自增运算符理解一次就再也不会忘。4.2 从易错点到工程实践这些基础怎么影响日常代码很多人觉得基础知识和业务开发隔着一层纱其实不然。举几个真实例子订单号比较用Long包装类型做订单量小没问题超过127后偶发不相等线上告警。原因是Long也有缓存和Integer一样的坑。随便翻开源项目这种Bug比比皆是。金额计算用double累加报表合计每次都差几分钱。应该全程用BigDecimal而且BigDecimal构造时用字符串构造器直接传double照样有精度问题。统计报表里分子分母都是int分母为0直接抛异常报表接口500。应该在计算前做分母判空和零值保护。状态机使用switch时少了break状态穿透订单状态错乱。应该用箭头语法或者把每个case独立成方法。这些坑没有一个属于高深技术但每一个都在生产环境里真实出现过。地基不稳上层建筑再漂亮也会塌。5. 给初学者的自查清单与面试答题思路5.1 我总结的Java基础自查顺序按我的经验Java基础知识自学完成后可以按这个顺序做一轮自查每一项都能立刻答上来才算真正过关八种基本类型的范围、默认值、字面量后缀规则包装类型的缓存范围哪些包装类有缓存缓存多大和equals在基本类型、包装类型、String、对象引用四种场景下的区别自动类型提升的完整规则int转float为什么是合法隐式转换i、i在赋值、运算、循环三处的求值过程整数除法、取余符号规则、除零的三种表现位运算的常用技巧和运算符优先级顺序三目运算符出现NPE的两种典型结构悬空else的配对规则switch的fall-through行为循环删除集合元素的正确写法和标签跳转的适用范围。5.2 一道题拆成三个层次的学练方法我教新人的方法是把每个知识点拆成三层来学。第一层是背结论。比如整数除法截断小数取余符号跟被除数Short短路不执行右侧这些结论必须烂熟面试时能脱口而出。第二层是写实验。结论背完立刻在IDE里写一个两三行的demo验证把输出记下来。注意要故意写错比如故意用替代去触发空指针故意用switch不写break看穿透结果亲手制造一次错误再纠正印象远比看文档深。第三层是找原因。用字节码、类型推断规则、语言规范去解释为什么是这样而不是死记。比如三目NPE的根本原因是表达式的静态类型必须统一成基本类型理解了类型统一规则就不用记那么多铺开的坑了。三层都走完这个知识点才算真正长在你身上面试怎么问都不慌。5.3 面试和笔试中的高分答题思路Java面试题里考察数据类型的常见形式是手写代码或口述输出结果。答题套路也有固定章法先判断表达式的类型再看运算符优先级最后分析求值顺序。以long x 5 / 2;为例不要急着说x是2而是先看5 / 2是两个int字面量结果是int的2再隐式转long得到2L。如果直说x2其实丢失了类型推断的关键步骤而面试官恰恰想听这几步推导。遇到涉及包装类型比较的题第一反应是查缓存范围和是否new对象。遇到三目表达式下意识检查两侧类型是否需要拆箱统一以及值是否可能为null。遇到流程控制题先画一下执行路径特别注意break是否写全、边界是否可能越界、删除元素是否用了迭代器。还有一点很关键回答时要把结论和推导过程分开。先给结论比如结果是6再用一到两句话说明推导i的跳跃步长、sum累加了哪些值、最后循环退出条件。这样不仅答案正确还能展现出清晰的工程思维这是面试官最看重的素质。我这几年面试过不少候选人凡是基础题答得干脆利落、能说明推导过程的后续项目上手也普遍快凡是每题都模棱两可的即使简历花团锦簇入职后大概率需要大量时间补课。基础这东西真的是一分耕耘一分收获。
阅读完成 · 觉得有帮助?
咨询建站