尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java零基础自学第二弹:基础语法、数组与面向对象避坑实录

Java零基础自学第二弹:基础语法、数组与面向对象避坑实录 第一弹发完有几位朋友催更也有同样零基础的朋友问我自己学到哪了。先交代一下进度上一篇写到了配置JDK环境变量、跑通第一个Hello World这一段时间我把Java基础语法往前推了一大截主要啃了数据类型、流程控制、数组、方法还有刚入门的面向对象。说实话越学越觉得“基础”这两个字一点都不基础每个知识点往深里一挖全是细节。这篇“第二弹”不打算写成系统教程还是按我的真实学习记录来哪些知识我一开始理解错了哪些坑花了我一下午我是怎么排查和纠正的全部如实写出来。如果你也是刚起步的Java新手或者准备转行做开发这篇应该能帮你省掉不少踩坑时间。老手看了如果有说错的地方欢迎直接指出来我还在学习中不玻璃心。1. 学之前先说说我的“笨办法”照抄、改改、盲写很多人学编程喜欢找一堆视频教程看完就觉得自己会了一动手写代码就卡住。我自己反正是从第一弹开始就发现看视频和写代码完全是两回事。我给自己定了一个循环验证的流程第一步看着视频或文档把代码原封不动敲一遍先保证环境能跑通第二步自己改动几个变量或条件猜一下运行结果再实际跑一下对比第三步合上代码凭记忆从零默写出来。这三个步骤走完我才敢说这个知识点“过了脑子”。这一弹的学习主线是这样的先从变量和数据类型入手把“存储”这件事弄清楚然后是运算符和流程控制让程序能“判断”和“循环”接下来数组登场配合循环做一些真正的数据处理再往后学方法把重复逻辑抽出来最后接触了面向对象开始理解“类”和“对象”的关系。学完之后我最大的感受是基础语法就像写文章的字和词单个拎出来都不难难的是组合在一起还能不犯错。而这中间最容易出问题的往往是那些“编译器不报错、运行结果却是错的”的地方。2. 数据类型和变量看着简单细节多到怀疑人生2.1 八大基本类型到底怎么记才不混Java里有8种基本数据类型我一开始死记硬背记了忘、忘了记。后来发现把它们按“整型、浮点、字符、布尔”分组再配合字节数和取值范围去理解就好记多了。类型字节数默认值取值范围 / 说明byte10-128 到 127short20约 -3.2万 到 3.2万int40约 -21亿 到 21亿最常用long80L很大赋值时数字末尾要加 Lfloat40.0f小数赋值时末尾要加 fdouble80.0d双精度小数默认浮点类型char2\u0000单个字符用单引号对应Unicodeboolean1规范未严格规定false只有 true 和 false记忆方法我用了一个“停车位”类比不同数据类型就像大小不同的车位byte 是摩托车位int 是普通车位long 是加长车位。变量往内存里存值就像车停进车位位置不够大就会出问题。这里有个新手特别容易踩的坑long类型赋值时末尾忘记加L。比如long num 1234567890123;编译会直接报错因为右边的数字默认是 int 类型已经超出范围了。正确写法是long num 1234567890123L;。同样 float 类型float f 3.14;会报错必须写成float f 3.14f;因为小数默认被当成 double。2.2 类型转换自动转、强转和丢失精度类型转换分两种。一种是自动类型转换从小范围往大范围转不需要写代码比如 byte 转 int、int 转 double 都行因为大容器装得下小东西。还有一种叫强制类型转换从大范围往小范围转需要手动加括号比如int i 130; byte b (byte) i; System.out.println(b);猜猜输出多少我当时理所当然以为会报错结果输出了 -126。原因是 byte 最大只能存 127130 超出范围数据溢出后被“截断”了。这也是个经典面试题用来考察对类型范围和二进制补码的理解。说实话我现在也只理解了个大概但至少知道了大转小一定要确认值在目标范围内否则结果会变得很离谱。还有个跟浮点相关的坑运行一下这句System.out.println(0.1 0.2);结果大概率不是 0.3而是一长串近似值。原因是浮点数在内存里用二进制表示0.1 和 0.2 都存不精确运算结果自然不精确。目前我只需要记住做金额计算时别用浮点类型后面学 BigDecimal 再解决。这个知识点也经常出现在面试题里属于八股文高频题。2.3 标识符命名规则我第一次被编译器教做人写变量名有多随意我最开始写了int 1num 10;编译直接报错。后来才知道Java标识符的规则是由字母、数字、下划线_、美元符号$组成不能以数字开头不能是关键字严格区分大小写。纠正了“不能数字开头”之后我又踩了第二个坑给变量取名class编译又报错因为 class 是关键字。后来习惯用 IDE 输入时看到黄色高亮或红色波浪线就知道命名有问题了。命名规范也很重要虽然编译器不强制但实际工作里会非常在意类名大驼峰比如StudentInfo方法名和变量名小驼峰比如studentName、getTotalScore常量名全大写加下滑线比如MAX_STUDENT_COUNT刚开始觉得这些规则麻烦但后来看代码看得多了就明白规范命名其实是“给人看的”不规范的代码别说别人看不懂过了几天自己都看不懂自己写的。3. 运算符和流程控制写代码的“语文语法”3.1 短路逻辑右侧代码到底执行没有运算符里最容易出隐蔽 bug 的是逻辑运算符的“短路”行为。和||在能确定结果时会直接跳过另一侧的计算。比如int a 10; boolean result (a 5) (a 10); System.out.println(a);输出结果是 10 而不是 11。因为a 5是 false整个表达式结果已经确定是 false右侧的a压根没执行。这件事在实战中很有用。最常见的写法是if (name ! null name.length() 0) { // 处理非空字符串 }如果把顺序反过来写成name.length() 0 name ! null当 name 为 null 时调用 length() 方法会直接抛出空指针异常。把可能为空的判断放前面、右侧放依赖前者的判断这是一条非常实用的编码经验。3.2 switch 穿透忘写 break 的下场学 if 分支还好学到 switch 时我犯了个典型错误每个 case 后面忘了写 break。上代码int day 3; switch (day) { case 1: System.out.println(星期一); case 2: System.out.println(星期二); case 3: System.out.println(星期三); default: System.out.println(未知); }运行结果把“星期三”“未知”全打出来了。原因就是 switch 的 case 默认不跳出会继续往下执行下一个 case这就是“穿透”。解决它的标准做法是每个 case 末尾加break;default 放在最后做兜底。现在的 Java 高版本也支持case -箭头语法自带 break 效果不过老代码里大量还是冒号写法两种都得能看懂。这段经历也提醒了我一件事报错不一定是红色异常结果不符合预期也是 bug。很多时候程序能运行但逻辑不对这种问题比编译报错更难查。3.3 for、while、do-while 到底什么时候用流程控制的循环我一开始总是分不清 for 和 while觉得两个都能完成循环为啥要分两个。后面做练习才发现它们的使用场景其实很清楚for 循环事先知道循环次数比如遍历数组。while 循环不知道具体次数只知道循环条件比如持续读取输入直到某个关键字结束。do-while 循环至少执行一次再判断条件比如“先弹出菜单让用户选选错再重新弹出”。举一个实际的需求写一个程序用户输入密码如果错误就重复输入直到密码正确为止。用 do-while 就很自然Scanner scanner new Scanner(System.in); String password; do { System.out.println(请输入密码); password scanner.nextLine(); } while (!123456.equals(password)); System.out.println(密码正确);这里有个细节我一开始没注意for 循环括号里声明的变量循环外不能用。for (int i 0; i n; i)里的 i 在循环外访问不到因为它只在 for 代码块内部生效。想在外面用下标就得在循环外先声明int i;。这属于作用域问题后面学方法、类的时候还会反复遇到。4. 数组我觉得我懂了直到被越界异常打脸4.1 创建数组的三种姿势和默认值数组是用来存“一串同类型数据”的容器。Java里创建数组的写法我见过三种// 方式一先声明长度后面再赋值 int[] arr1 new int[5]; // 方式二直接初始化 int[] arr2 {1, 2, 3, 4, 5}; // 方式三声明加赋值 int[] arr3 new int[] {1, 2, 3, 4, 5};用方式一创建数组的时候里面的元素会自动赋默认值int 数组默认全是 0double 数组默认是 0.0boolean 数组默认是 falseString 数组默认是 null。这个“默认值”机制我一开始完全不知道后面学类成员变量时又遇到一次现在算是刻进脑子了。还有一个重要特性数组一旦创建长度就不能改变。想加元素不好意思没有这个方法。后面学的 ArrayList 集合才是动态长度的数组主要用于元素个数固定的场景。4.2 用“一排座位”理解下标和 length数组为什么老是越界我从根本上把它理解成“教室里的座位”一排有 6 个座位编号从 0 开始所以最后一个座位的编号是 5。数组长度 length 是 6但下标范围是 0 到 length-1。遍历数组的标准写法是for (int i 0; i arr.length; i) { // 使用 arr[i] }注意这里是严格小于arr.length不是小于等于。我有一阵老写成i arr.length运行结果就是数组越界异常。因为当下标等于长度时已经跑到最后一排座位的墙外边去了。4.3 实战计算全班平均成绩数组配合循环能做的事情太多了。我照着练习册写了个“计算一组学生平均成绩”的小程序代码长这样int[] scores {85, 92, 78, 65, 88}; int sum 0; for (int i 0; i scores.length; i) { sum scores[i]; } double average (double) sum / scores.length; System.out.println(平均成绩 average);这里有两个地方值得注意。第一累加变量 sum 必须初始化为 0否则默认值是 0不对如果你在方法内部声明int sum;就直接用编译器会报错“变量 sum 未初始化”这是局部变量和成员变量的一个重要区别后面会细说。第二求平均值时我一开始写的是sum / scores.length结果是 81把小数全丢了。因为整数除整数结果还是整数。必须把 sum 强转成 double或者把除数写成scores.length * 1.0才能得到真正的平均值。这下我对“类型转换”那节的知识理解又深了一层。4.4 完整排查一次 ArrayIndexOutOfBoundsException踩越界异常是个必经之路。我记得最清楚的一次报错信息长这样Exception in thread main java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5 at Main.main(Main.java:8)这个异常信息其实已经把问题讲得非常清楚了下标 5 超出了长度 5 的范围。我当时的代码是int[] arr {1, 2, 3, 4, 5}; for (int i 0; i arr.length; i) { System.out.println(arr[i]); }排查过程是这样的第一步看异常类型是 ArrayIndexOutOfBoundsException说明和数组下标相关第二步看异常信息里的数字是 5而数组长度是 5说明访问到了不存在的下标第三步看行号定位到 for 循环那一行第四步检查循环条件发现写成i arr.length改成i arr.length第五步重新运行问题解决。很多人一看到异常报错就慌其实 Java 的异常信息已经给了你排查线索比我之前用 Python 时遇到的一堆 Traceback 友好多了。以后再遇到类似报错第一反应应该是读异常信息而不是直接复制粘贴搜“怎么解决”。5. 冒泡排序第一个“算法”一下午都在和循环较劲数组和循环组合到一起能做的事就多了排序是最经典的一类。我学的第一个排序算法是冒泡排序刷题网站和面试里都很常见属于Java里像“Hello World”一样的基础操作。冒泡排序的直观理解是从头开始相邻两个数两两比较如果前面比后面大就交换位置。这样一趟下来最大的数就像气泡一样浮到了最后。重复做 N-1 趟所有数就从小到大排好了。下面是我最终跑通的版本int[] arr {5, 1, 4, 2, 8}; for (int i 0; i arr.length - 1; i) { for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } for (int i 0; i arr.length; i) { System.out.print(arr[i] ); }我一开始根本没看懂为什么内层循环要写arr.length - 1 - i。后来画了个图才明白第一趟比完最后一个位置已经是最大值下一趟就不用再和最后一个比了第二趟结束倒数第二个位置也定了。所以每跑一趟内层比较次数就少一次正好对应- i。这中间我还犯过一个经典错误把内层循环写成j arr.length然后一运行就报越界异常。原因在访问arr[j 1]时当 j 等于 length-1访问的其实是arr[length]又踩中了上一章说的“座位编号从0开始”的坑。这算是对数组越界知识点的二次巩固。冒泡排序还有一个容易犯错的地方是交换逻辑。交换两个变量必须用临时变量 temp顺序是先存 arr[j] 到 temp再把 arr[j1] 赋给 arr[j]最后把 temp 赋给 arr[j1]。我一开始直接写arr[j] arr[j1]; arr[j1] arr[j];结果两个数变成了同一个值彻底蒙了。实际开发里Java提供了Arrays.sort()方法可以直接排序原理上用的是更高效的快速排序或归并排序但初学阶段亲手写一遍冒泡排序依然非常值得。它能帮你把循环嵌套、数组访问、变量交换这些基础功一次性练到位也为后面学更复杂的算法打了底子。6. 方法把重复逻辑收进“盒子”6.1 为什么一定要写方法连续写了三次“求数组平均成绩”的代码后我意识到一个问题相同的代码复制来复制去又丑又难维护。如果有一天平均数规则改了我得改好几个地方。这就是方法存在的意义把一段逻辑打包成一个可以反复调用的“盒子”。方法的好处是显而易见的一处定义多处调用主流程变清爽可读性提高修 bug 时只改一个地方而不是全项目搜代码。6.2 方法的结构和参数使用方法的基本格式是修饰符 返回值类型 方法名(参数列表) { 方法体 return 返回值; }比如我把求数组最大值写成方法public static int getMax(int[] arr) { int max arr[0]; for (int i 1; i arr.length; i) { if (arr[i] max) { max arr[i]; } } return max; }方法名我喜欢用动词开头的驼峰命名比如getMax、printInfo这样调用处读起来像一句自然语言。没有返回值的方法用void表示。void这个词我一开始看了好几遍才记住不是真的“没有类型”而是特意声明“这个方法不返回任何东西”。6.3 值传递的坑为什么方法里改了外面没变这个知识点花了我很长时间才彻底接受。直接看代码public static void change(int num) { num 100; } public static void main(String[] args) { int a 10; change(a); System.out.println(a); // 输出还是 10 }为什么 change 里面把 num 改成 100外面 a 还是 10因为 Java 参数传递是值传递传给方法的是变量值的副本方法里改的是副本影响不到原变量。但数组这种“引用类型”表现就不一样public static void changeArray(int[] arr) { arr[0] 100; } public static void main(String[] args) { int[] nums {1, 2, 3}; changeArray(nums); System.out.println(nums[0]); // 输出 100 }数组传入方法后修改数组元素原数组也会变。区别在哪我的理解是基本类型传的是“值”引用类型传的是“地址”方法通过地址找到同一个数组动手改等于在原数组上操作。Java 管这个叫值传递只是引用类型的“值”本身就是一个指向对象的引用地址。这个坑在以后写的业务代码里会经常遇到而且很难查。现在知道这个机制后面看别人的代码就少懵几回。6.4 方法重载同名方法参数不同就各干各的重载这个概念第一次接触时觉得抽象其实就是同一个方法名只要参数列表不同就行。参数列表不同可以是参数个数不同也可以是参数类型不同。public static void print(int num) { System.out.println(数字 num); } public static void print(String str) { System.out.println(字符串 str); } public static void print(int a, int b) { System.out.println(两个数字 a 、 b); }上面这三个print方法重载关系成立。调用时 Java 会根据你传的参数来自动匹配对应版本。System.out.println其实就是一个典型的重载方法它重载了无数个版本才能接各种不同类型的参数。学重载时要注意只有返回值类型不同不算重载因为光靠返回值无法确定调用哪个方法编译器会报错。7. 面向对象初体验类、对象、封装到底在说什么7.1 类就是图纸对象才是房子学前面的语法时代码都是按顺序从头写到尾的这是“面向过程”。到了面向对象这一章思路要转一个大弯。书上最经典的解释是类是一张设计图纸对象是根据图纸造出来的具体房子。图纸本身不能住人只有按照图纸 new 出来的房子才能住人。对应到代码上定义一个“学生类”public class Student { String name; int age; public void sayHello() { System.out.println(大家好我是 name 今年 age 岁); } }使用类创建对象Student stu1 new Student(); stu1.name 小明; stu1.age 18; stu1.sayHello();new Student()就是在“按图纸造房子”stu1 就是一个具体的学生对象。类里只定义了属性和行为不占内存真正创建对象时才在内存里分配空间每个对象有自己独立的一份属性值。这个“类 vs 对象”的比喻我一开始觉得太简单实际一写代码才发现类里写属性和方法对象里存取属性、调用方法这个主语和宾语的关系经常搞混。多敲几次之后手比脑子先记住了。7.2 成员变量和局部变量默认值把我坑了成员变量定义在类里面、方法外面局部变量定义在方法里面。两者有个关键区别成员变量有默认值局部变量没有。public class Student { int age; // 成员变量默认值是 0 String name; // 成员变量默认值是 null public void test() { int score; // 局部变量没有默认值 System.out.println(score); // 编译报错可能尚未初始化 } }我一开始很困惑为什么成员变量可以直接输出局部变量必须先赋值原因在于 Java 要求方法内部变量必须显式初始化否则缺少确定值编译器无法保证安全。而成员变量由 VM 在创建对象时统一做了默认初始化。还有一个作用域的坑方法里可以定义一个和成员变量同名的变量但方法内访问到的是局部变量这叫做“变量遮蔽”。想访问成员变量可以用this.age。比如public void setInfo(String name, int age) { this.name name; this.age age; }这里的 this 就代表“当前这个对象”起到区分成员变量和参数的作用。我学到这里才渐渐读懂之前看的代码里很多this到底在说什么。7.3 封装private 和 getter/setter类有了属性也有了但如果外部代码能直接改属性值就会出现离谱的数据。比如学生年龄被改成 -5虽然语法上合法逻辑上不合理。这时就要用private私有化属性外部不能直接访问只能通过公共方法读取和修改。public class Student { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name name; } public int getAge() { return age; } public void setAge(int age) { if (age 0 || age 150) { System.out.println(年龄不合法); return; } this.age age; } }这样一来外部要设置年龄就必须调用setAge而 setAge 里可以做合法性校验防止不合理的数据进入对象。封装的意义就在这里不暴露内部实现细节只提供受控的访问入口。我一开始嫌 getter/setter 写起来麻烦十个属性要写二十个方法。后来理解了这些方法不是“摆设”而是给数据加了一道“安检门”。实际开发里一些框架比如 Spring Boot 的 JavaBean 规范甚至要求很多类必须提供这些方法这也能解释网络上搜到的很多 Java 项目源码里为什么到处都是 getter/setter。顺带提一下构造方法new Student()后面那个括号其实调用的就是构造方法。如果我们不写Java会默认给一个无参构造器。如果写了有参构造器默认无参构造器就没了需要自己手动补上。这是新手很容易掉进去的坑我见过好几个小伙伴在这一步调了半天的错。8. 本弹踩坑清单和给同为新手的几个建议这一路学下来踩的坑远比上面写的多我把它们汇总成一个“错题本”每次动手写代码之前扫一眼确实能避免不少重复错误数组遍历把 arr.length写成 arr.length造成越界异常冒泡排序内层循环忘了- i或者直接把 j 写成 j1 导致访问越界long 和 float 赋值忘加后缀 L / fswitch 的 case 忘记写 break导致穿透 和 || 两侧顺序没注意触发空指针强制类型转换导致数值溢出或精度丢失局部变量没初始化就直接使用编译报错swap 两个变量时没用临时变量越换越乱整数相除没转 double直接丢小数给同样零基础自学的朋友几个真实心得。第一别追求速度一个知识点搞不懂就在 IDE 里反复改代码、看输出那些输出会告诉你哪里理解错了速度慢一点没关系。第二代码一定要自己敲复制粘贴看起来都会等于没会。第三错题本强烈推荐每遇到一个坑把报错信息、出错代码、正确写法记到 Markdown 或笔记软件里一段时间后回头看你会发现自己真的进步了。下一步我准备开始学字符串的常用操作、StringBuilder还有集合框架里的 ArrayList 和 HashMap。再往后我会找一些蓝桥杯里的基础题目练手尤其是数字处理、排序相关的正好把这一弹学的数组和流程控制用起来。等第三弹再见希望那时候我已经能写一个像样一点的小项目出来了。
返回列表