☰
Java基础必知:从语法到并发,夯实核心知识体系
2026/9/30 12:35:17 网站建设 项目流程

“Java基础必知”这几个字,我当年学的时候觉得轻飘飘的,直到面了三十多家公司、又被别人问了无数次“你Java基础怎么样”之后,才明白这里面的分量。基础不是背几个关键字、会写个HelloWorld,而是你面对一个需求时,能在脑子里直接形成“用哪个集合、怎么处理异常、要不要开线程、内存会不会炸”的判断链。这篇内容不整虚的,按我实际带人、复习、面试的经验,把Java基础里最该吃透的部分拆开讲清楚,适合刚入门的、准备实习的、以及工作一两年想回头补地基的朋友。

1. 基础的地基:语法和数据类型永远先于框架

1.1 为什么语法反而是最容易被忽略的逆袭点

很多人一上来就Spring Boot、MyBatis,结果写个循环都分不清==和equals,一个HashMap遍历时删元素直接抛异常,还以为是框架的问题。实际上,Java基础的第一层不是“会用IDE”,而是“能不看IDE提示写出没有低级错误的代码”。

我见过太多简历写着“熟练掌握Java”,却答不上来byte和short的取值范围,也说不清float和double的精度差异。这些知识不“高级”,但它们是判断一个人是否真正写过代码的试金石。语法层的细节,比如switch对字符串的支持、try-with-resources的自动关闭、lambda表达式的变量捕获限制,这些不是八股,是你在真实业务里每天都会遇到的工具。

如果你连List和Set的区别都要想半天,连StringBuilder和StringBuffer为什么存在都说不清楚,那后面的并发、JVM、设计模式全都是空中楼阁。基础语法阶段,我建议你训练一个习惯:每天手写五个小片段,比如字符串反转、整数去重、文件按行读取,写完再想想有没有更省内存的写法。这个阶段不需要框架,一个main方法就能练。

1.2 数据类型和运算符的坑,面试官最爱从这里开刀

int a = 1; Integer b = 1; System.out.println(a == b);输出是什么?Integer c = 128; Integer d = 128; System.out.println(c == d);又是为什么?前者是int和Integer比较自动拆箱,后者是Integer缓存范围在-128到127,128不在缓存里,所以c和d是两个对象。这种题不是刁难你,而是看你有没有真读过源码或者踩过坑。

我建议你把基础数据类型全家桶记熟:byte8位、short16位、int32位、long64位、float32位、double64位、boolean不确定(看虚拟机)、char16位无符号。然后重点理解自动装箱和拆箱在循环里的性能损耗。比如`

Integer sum = 0; for (int i = 0; i < 100000; i++) { sum += i; // 每次都拆箱、加、再装箱 }

这个代码跑了多久?我实测比int sum慢了将近十倍。你写业务可能感觉不到,但在高并发场景下,这类问题就是拖垮接口的元凶。

另一点是位运算。&、|、^、<<、>>、>>>这些,不是让你去炫技,而是很多框架源码里都在用。比如HashMap计算索引用的是(n - 1) & hash,就是为了让散列值落在数组范围内。你懂了位运算,看源码就会顺畅很多。所以基础阶段的运算符,不要只学算术运算符,位运算和移位运算必须吃透。

1.3 流程控制:写得像人话,比写得“聪明”重要

if-else、switch、for、while这些,大家都会写。但“会写”和“写好”差距很大。一个典型问题是嵌套地狱:三层循环里套五个if,后面维护的人想骂人。我的经验是:条件判断能早返回就早返回,循环能增强for就尽量别用下标,复杂的业务判断用switch加枚举替代连续if。

还有一个性能细节:循环里尽量不要调用size()方法以外的重量级操作,比如在for里拼接字符串,一定要用StringBuilder而不是String +。很多人知道这个道理,但不知道为什么——因为String是不可变对象,每次+都会创建新对象,旧对象等GC回收。而StringBuilder内部是可变字符数组,直接追加,快一个数量级。

流程控制里还有个容易被忽略的break和continue的标签用法,虽然不常用,但看老代码时偶尔会遇到。比如:

outer: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { if (j == 5) break outer; } }

这能直接跳出两层循环。我没有推荐你在业务里这么写,但碰到时至少知道它是什么意思。基础的边界在于“能看懂”和“能正确使用”,而不是故意写冷门代码。

2. 面向对象编程:从“三大特性”到“设计直觉”

2.1 封装:不仅是为了安全,更是为了“能改”

很多人背封装的定义是“把属性私有,提供getter/setter”。但这样理解太浅了。封装的真正价值是“隐藏变化”。比如你有一个User类,年龄字段age,如果直接公开,那么外面可以随便赋一个-5。如果你提供一个setAge(int age)方法,在里面做校验,就能保证对象永远处于合法状态。

我更愿意用“最小暴露面”来解释封装:你只暴露别人必须用的方法,内部实现随便改。比如从数组换成ArrayList,对外接口不变,调用方无感知。这就是封装的威力。实际开发中,设计类的时候先问自己:这个类的字段有哪些是不希望外部直接改的?哪些操作应该合并成一个原子方法?比如“转账”应该是一个方法,而不是让调用方先调“减钱”、再调“加钱”,那样中间出异常就完了。

2.2 继承:组合优先于继承,这句话基础阶段就要记住

继承是Java面向对象的招牌特性,但它也是被滥用最多的。我记得刚工作时写了一个Manager extends Employee,后来需求变成“一个人可以既是Manager又是Engineer”,继承体系直接崩了。后来学了设计模式才知道,组合(has-a)通常比继承(is-a)更灵活。

基础阶段你需要理解继承的机制:类只能单继承、接口可以多实现;子类构造器必须调用父类构造器(隐式调用无参构造器,如果没有就编译报错);方法重写要注意访问权限不能更低,抛出的异常不能更宽。这些规则背后都有合理性。

举个例子:

class Animal { void speak() { System.out.println("动物叫"); } } class Dog extends Animal { @Override void speak() { System.out.println("汪汪"); } } Animal a = new Dog(); a.speak(); // 输出汪汪,动态绑定

这个例子能解释“什么是多态”。但要更进一步:为什么a能调用speak却调不到Dog独有的方法?因为编译看左边类型Animal,运行看右边对象Dog。这个知识点面试时经常延伸成“静态分派和动态分派”,你需要理解方法调用的本质:JVM在运行时根据实际类型找到方法地址再执行。

2.3 多态:接口才是设计的核心

我做了几年项目,最大的体会是:Java基础里最值钱的是接口思维。List是一个接口,ArrayList和LinkedList是实现;Map是接口,HashMap和TreeMap是实现。你写代码时如果面向接口编程,比如方法参数写List<String>而不是ArrayList<String>,那么以后换实现类就不会影响调用方。

多态的另一个重要应用是策略模式、模板方法模式等设计模式的底层。比如一个排序方法,接收一个Comparator接口,你在调用时传入不同的实现,就能改变排序规则。这就是“对扩展开放、对修改关闭”的基础。

在基础阶段,我建议你亲手写一个简单接口示例:定义一个PayService接口,有pay()方法,然后写AliPay、WechatPay两个实现类,再写一个OrderService接收PayService参数。跑通之后,你就能理解为什么框架里大量使用接口了。

2.4 抽象类和接口:选择困难时的判断依据

这个问题面试必问:“抽象类和接口的区别是什么?”很多人背答案:抽象类可以有构造器、可以有成员变量、可以有普通方法;接口从Java 8开始可以有默认方法和静态方法。但这些区别背后的目的才是关键。

抽象类用于“共享状态”和“共享行为”,它描述的是一种“是什么”的继承关系,比如AbstractList给子类提供了公共的迭代逻辑。接口用于“规范行为”和“能力约束”,比如Comparable表示“可以比较”。当你纠结时,问自己:多个实现类之间是否要共享代码和字段?如果是,用抽象类;如果只是定义一组行为契约,用接口。

还有一个经验:如果一个类已经继承了某个抽象类,又想增加另一种能力,只能通过接口实现。因为Java单继承,这个限制逼着你思考“组合优于继承”。基础阶段可以多画类图,哪怕用纸笔,把继承关系、实现关系画清楚,比单纯看文字理解深得多。

3. 集合框架:用的最多的容器,也是面试的重灾区

3.1 集合的整体结构:先记体系,再记细节

Java集合框架是基础里的核心,也是面试八股文的高频区域。但我不建议死记“ArrayList底层数组、LinkedList底层链表”这种一句话答案。你要理解为什么底层结构不同,进而带来什么差异。

集合主要分两大体系:Collection(单列集合)和Map(双列集合)。Collection下面又分List(有序可重复)、Set(无序不可重复)、Queue(队列)。Map则存键值对。这个体系图必须刻在脑子里。

我再强调一点:不要用Hashtable和Vector了,它们是被历史淘汰的同步容器,性能差。面试时提到它们,只是为了说明Collections.synchronizedList和ConcurrentHashMap的出现背景。

3.2 ArrayList的扩容机制:为什么默认容量是10?

ArrayList底层是Object[],初始容量如果没指定,默认是10。当元素个数超过容量时,会扩容为原来的1.5倍(oldCapacity + (oldCapacity >> 1)),然后Arrays.copyOf将旧数组元素复制到新数组。这个机制的代价是:频繁扩容会导致复制开销。所以如果你知道大概数据量,最好直接new ArrayList<>(expectedSize)。

有朋友问我:“为什么默认是10,不是8或16?”其实这个值没有严格最优解,是JDK开发者的经验取值。你不需要纠结为什么,需要记住的是扩容是O(n)操作,如果能预估容量,一定要提前指定。

还有一个细节:ArrayList.subList返回的是原集合的视图,不是新集合。对subList修改会影响原集合,反之亦然。很多人踩过这个坑,我建议在基础阶段就亲手验证一遍,加深记忆。

3.3 HashMap的底层原理:从数组+链表到红黑树

HashMap是面试的重中之重。我建议从这几个层次去理解:第一层,它是数组加链表的结构;第二层,当链表长度大于等于8且数组长度大于等于64时,链表会转成红黑树;第三层,扩容和索引计算。

先说索引计算:hash方法对key的hashCode进行扰动,然后(n - 1) & hash得到数组下标。因为n是2的幂,所以n-1的低位全是1,与运算等同于取模,但性能更高。这也是为什么HashMap初始容量是16,扩容是翻倍,始终保持2的幂。

再说put流程:先算hash,定位到数组位置。如果该位置为空,直接放入;否则判断key是否相等,相等就覆盖;否则走链表或红黑树。在链表尾部(或树中)插入后,如果键值对数量超过阈值,就扩容。

这里有个容易混淆的点:链表转红黑树的阈值是8,但还要求数组长度不小于64。如果数组长度小于64,会先扩容而不是转树。原因?链表不长时遍历成本不高,而树节点占用的空间是普通节点的两倍,转树还要维护树的平衡,不划算。

最后说一下HashMap的线程安全问题。它不是线程安全的,多线程put可能导致死循环(JDK7头插法的问题,JDK8修改为尾插法后不再有这个问题,但数据覆盖和并发修改异常仍然存在)。如果你需要并发安全,用ConcurrentHashMap,它通过CAS加synchronized锁定桶头节点来保证线程安全。

3.4 遍历集合的最佳姿势:for-each和Iterator的差别

遍历是基本功,但很多人不知道for-each的本质是Iterator。比如:

for (String s : list) { if (s.equals("a")) list.remove(s); }

这段代码会抛ConcurrentModificationException,因为迭代器发现集合被外部修改了。如果你想在遍历中删除元素,必须用Iterator的remove方法:

Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("a")) it.remove(); }

这两个例子的区别,你最好在本地跑一遍,报错信息会让你记得更牢。基础阶段注意一点:尽量用entrySet()遍历HashMap,而不要用keySet()再去get一次,因为后者每次都要重新计算hash,性能差一些。看起来是细节,但在大数据量下差距明显。

4. 异常、String和常用工具:最容易忽视的“送分题”

4.1 异常体系:不要只会try-catch

Java异常分两大类:Error和Exception。Error是JVM层面的严重问题,比如OutOfMemoryError,程序员不应当catch。Exception下又分RuntimeException(非检查异常)和CheckedException(检查异常)。RuntimeException不用强制捕获,像NullPointerException、ArrayIndexOutOfBoundsException;CheckedException必须处理,比如IOException、SQLException。

实际项目中,我见过很多人滥用异常,把try-catch包住整个方法,然后吞掉异常:

try { // 业务逻辑 } catch (Exception e) { // 什么都不写 }

这是很坏的习惯,异常被吞了,出问题时无从查起。正确做法是:能处理就处理,不能处理就抛出。捕获后至少要打日志,别用空catch块。还有一个点是,不要用异常控制业务流程,比如用NumberFormatException判断字符串是否数字,这是非常差的习惯,应该用正则或Character.isDigit去判断。

关于自定义异常:当系统需要表达特定业务错误时,可以定义自己的BusinessException,继承RuntimeException,这样不用在每个调用处强制处理,交由全局异常处理器统一处理。这也是基础,但很多人到工作才意识到。

4.2 String不可变性的真正意义

String被final修饰,不可变。为什么?第一,字符串常量池需要保证同一个字符串引用的一致,如果可变,池里就被篡改了;第二,缓存hashCode,比如HashMap经常用String作key,不可变让hashCode计算一次缓存起来,性能好;第三,安全,比如网络连接地址、文件路径不希望被修改。

理解不可变性,还要理解字符串拼接。我见过有人这样写:

String s = ""; for (int i = 0; i < 10000; i++) { s += i; }

这样会产生上万个中间字符串,非常浪费。正确写法是:

StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(i); } String s = sb.toString();

StringBuffer和StringBuilder的区别是前者线程安全(方法加了synchronized),后者不是。单线程下优先用StringBuilder,速度更快。

4.3 equals和hashCode:为什么重写equals必须重写hashCode?

这是面试经典题。equals判断两个对象内容相等,hashCode返回对象的哈希值。规则是:如果a.equals(b)为true,那么a.hashCode()必须等于b.hashCode()。反过来不成立。

如果不重写hashCode,会怎样?比如你有一个Person类,根据id相等判断两个Person相等,但没重写hashCode。把Person放入HashSet时,两个对象hashCode不同,会被放进不同的桶里,导致“相等对象被重复存储”。这是设计的逻辑漏洞。

所以基础阶段,你只要使用Hash相关的集合,就必须同时重写这两个方法。IDE可以自动生成,但你要理解生成的模板为什么那样写:用业务关键字段生成hashCode,比如Objects.hash(id, name)。

4.4 常用工具类的正确打开方式

Math、Arrays、Collections这些工具类属于基础,但用法细节很多。比如Arrays.asList返回的是一个固定大小的List,不是ArrayList,你调用add会抛UnsupportedOperationException。再比如Collections.sort可以直接对List排序,传入Comparator就可以定义规则。这些工具类能让你少写很多代码。

我建议你在基础阶段就把Objects.equals、Objects.hash、StringUtils(Apache Commons或Spring的)用起来。它们能减少空指针异常。比如你要比较两个字符串,直接a.equals(b)时a可能为null,应该写成Objects.equals(a, b)。这个习惯能让你少踩很多坑。

5. 排序算法:不只是背代码,要能分析复杂度

5.1 为什么Java基础阶段要学排序算法

很多人问:“Java开发用得着排序算法吗?直接Collections.sort不就好了?”但面试考排序,不是让你造轮子,而是考察你是否具备“计算思维”和“基本功”。手写一个冒泡排序,就能判断你有没有真正理解循环、数组、交换变量。更深一层,排序算法体现了复杂度分析、分治思想、递归能力,这些是后续学习搜索、图算法、动态规划的地基。

而且,Java基础面试题里,“手写一个排序并讲清时间复杂度”几乎是必考题。你可以不背快速排序的每一行,但你至少要能写出稳定、正确的冒泡或选择排序,并说出它们在最好、最坏情况下的复杂度。

5.2 冒泡排序:最直观也最容易写错的排序

冒泡排序的核心是相邻元素两两比较,把大的往后移。边界条件是每轮比较次数递减。我见过很多人写的冒泡排序因为内层循环边界不对,导致多比较一次或者漏比较。先看标准写法:

public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } if (!swapped) break; // 没有交换说明已经有序 } }

注意这里加了一个swapped标志位,如果一轮下来没有交换,就提前退出,最好情况(已经有序)时间复杂度从O(n²)降到O(n)。这个优化是加分项,也体现你有优化意识。

时间复杂度分析:外层循环n-1轮,内层循环平均n/2次,所以最好情况O(n),最坏和平均O(n²),空间复杂度O(1),稳定。

5.3 选择排序和插入排序:各有场景

选择排序是每轮找到最小元素的下标,然后和当前位置交换。不稳定,不管数据原本是否有序,都是O(n²)。插入排序则是扑克牌式的,把每个元素往前面已排好序的部分插入,适用于“基本有序”的数据,数据量小时效率高,最好情况O(n)。

你要是能把这三种排序写清楚,并且能解释它们的稳定性和复杂度,基础关就过了一半。我更推荐你掌握插入排序,因为它在工程上还有应用,比如JDK的Arrays.sort对小型数组用的就是插入排序(在优化后的快速排序里,当递归切割到一定小尺寸时会切换到插入排序)。

5.4 快速排序:面试的高频手写题

快速排序是实际运用最广的排序算法之一,也是“分治”思想的典型代表。思路是选一个基准(pivot),把所有小于基准的元素放左边,大于的放右边,然后递归对左右子数组排序。标准写法基于双指针:

public static void quickSort(int[] arr, int low, int high) { if (low >= high) return; int pivot = partition(arr, low, high); quickSort(arr, low, pivot - 1); quickSort(arr, pivot + 1, high); } private static int partition(int[] arr, int low, int high) { int pivot = arr[low]; // 取第一个元素为基准 int left = low; int right = high; while (left < right) { while (left < right && arr[right] >= pivot) right--; arr[left] = arr[right]; while (left < right && arr[left] <= pivot) left++; arr[right] = arr[left]; } arr[left] = pivot; return left; }

快速排序的平均时间复杂度O(n log n),最坏情况O(n²)(当数组已经有序且基准总是取到边界元素时)。可以通过随机选择基准来避免最坏情况。空间复杂度O(log n),是递归调用栈的开销。快排是不稳定的排序。手写时最容易出错的是partition里的边界条件,建议你在纸上走一遍[3,1,5,4,2]的流程,加深理解。

5.5 排序算法的稳定性:为什么要关心?

稳定性指相同元素的相对顺序不改变。比如你按成绩排序,有两个学生成绩相同,希望先按学号排的次序保留。如果算法不稳定,可能会把次序打乱。为什么基础阶段要学这个?因为实际业务中常有“多条件排序”,比如先按时间倒序、再按优先级正序,如果用稳定排序,第二趟排序可以保持第一趟的顺序。

在Java基础面试中,你可能被问到“Collections.sort用的什么算法”?其实它底层是Arrays.sort,对对象使用归并排序(稳定的),对基本类型使用快速排序(不稳定的)。这是个很有意思的细节,体现了JDK对稳定性的照顾:基本类型相同值无差别,不需要稳定;对象排序则要保持相等对象的相对顺序。

6. 多线程与并发基础:理解Thread和锁的底层逻辑

6.1 线程与进程:别再背概念了,要看它们怎么工作

进程是资源分配的基本单位,线程是CPU调度的基本单位。一个进程至少有一个线程,Java程序天生多线程:main线程之外,还有GC线程、编译器线程等。线程之间共享进程内存(堆和方法区),但每个线程有自己的虚拟机栈和程序计数器。这是你理解并发问题的前提。

操作系统的线程调度可能是分时的,也可能优先级抢占。Java中Thread.yield()是提示当前线程让出CPU,但不保证。sleep和wait的区别也是基础中的基础:sleep是Thread的静态方法,不释放锁;wait是Object的方法,必须在同步块中调用,会释放锁并等待notify或notifyAll唤醒。这个区别后面会直接影响你有没有死锁。

6.2 创建线程的几种方式:Runnable比Thread更好

Java创建线程的方式有几种:继承Thread、实现Runnable、实现Callable(可以有返回值)、使用线程池ExecutorService。我推荐优先实现Runnable或Callable,因为Java单继承,实现接口能保留类的扩展余地;而且任务与线程分离,更符合“将业务逻辑交给线程执行”的思维。

一个常见的面试坑:“用start()还是run()启动线程?”start()会创建新线程并执行run();直接调用run()只是在当前线程执行普通方法,没有多线程效果。这个一定要亲手验证:在run()里打印当前线程名称,用两种方式跑一下,你会直观看到差别。

6.3 synchronized的原理:锁的升级之路

synchronized是Java内建的同步机制。在JDK 1.6之后,JVM对它做了大量优化,锁可以从无锁状态升级为偏向锁、轻量级锁、重量级锁。基础阶段不需要把HotSpot源码背下来,但你需要明白三个概念:锁对象、临界区、锁的互斥性。

public synchronized void add()锁的是this;public static synchronized void add()锁的是Class对象;synchronized(lock){ }锁的是指定对象。如果锁对象不是同一个,两个线程可以同时进入方法,这会导致数据不一致。所以“锁必须保证是同一个对象”比“加不加synchronized”更重要。

一个经典案例:卖票。两个线程同时操作票数count,不加锁可能出现负数。正确的加锁方式是把count--放进synchornized块,锁是this。但如果你在别的地方用不同的锁对象,还是白搭。基础阶段建议多写几个并发卖票的例子,实时打印票数和线程名,理解临界区的作用。

6.4 volatile:保证可见性,但不保证原子性

volatile修饰的变量具有可见性:一个线程修改后,其他线程能立刻看到。实现原理是内存屏障和缓存一致性协议(如MESI)。但它不能解决原子性问题。比如volatile int count的count++,实际上包含了“读取-修改-写入”三步,并发下依然会丢失更新。

正确使用volatile的经典场景是状态标志位:

volatile boolean running = true; // 线程A循环 while (running) {...} // 线程B修改 running = false; 线程A能立刻看到

这个例子能帮你区分“可见性”和“原子性”。基础阶段不要纠缠于底层内存模型太深,但至少要能口头解释:“为什么普通变量在并发下可能读到旧值?因为线程有自己的工作内存,volatile可以强制刷新到主内存。”

6.5 死锁的产生与排查:这是面试加分项

死锁产生的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。Java代码层面最常见的死锁就是两个线程各持有一把锁,然后互相等对方释放。写个示例很容易:

Object lockA = new Object(); Object lockB = new Object(); // 线程1:synchronized(lockA) { 等一下; synchronized(lockB) {...} } // 线程2:synchronized(lockB) { 等一下; synchronized(lockA) {...} }

如果两个线程同时执行,就可能卡死。排查方法:先用jps查Java进程id,再jstack pid看线程转储,你会看到“Found one Java-level deadlock”的提示,还有线程等待的锁信息。基础阶段建议学会这个工具,面试时提到“我用jstack排查过死锁”,会让面试官眼一亮。

7. 输入输出与文件处理:基础中的务实技能

7.1 IO流体系:字节流和字符流的分工

Java IO分为字节流和字符流。字节流以InputStream/OutputStream为基类,处理二进制数据;字符流以Reader/Writer为基类,处理文本。为什么会有两套?因为字符流内部做了编码转换,读文本时更方便;而图片、视频等必须用字节流。

基础阶段需要你至少熟练使用FileInputStream、FileOutputStream、BufferedReader、BufferedWriter。记住一个原则:流用完之后一定要关闭。推荐使用try-with-resources语法,自动关闭:

try (BufferedReader reader = new BufferedReader(new FileReader("a.txt"))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }

这个语法在Java 7引入,它会自动调用close(),还能处理关闭时的异常。基础阶段就养成这个习惯,比以后被线上文件句柄泄漏教做人好得多。

7.2 NIO:理解缓冲区和非阻塞概念

NIO(New IO)不是面试必考,但你要知道它的核心价值:面向缓冲区、通道(Channel)和选择器(Selector)。传统IO是阻塞的,一个线程读文件时卡住,其他任务就无法进行;NIO支持非阻塞模式,让一个线程能管理多个连接,Netty的底层就是基于NIO。

基础阶段我建议你把FileChannel读取文件写过一遍,感受一下“通道+缓冲区”的方式和传统IO的差异。很多人在面试时会提到NIO,但只说出名词,说不出ByteBuffer的flip()和clear()有什么区别,这种就露怯了。实际解释就是:flip()把写模式切换为读模式,把position归零、limit设为原position;clear()则重置为写模式,丢弃剩余数据。

7.3 序列化:对象如何变成字节流

Java对象要持久化到文件或网络传输,需要实现Serializable接口。这个接口没有方法,只是一个标记。你需要理解的是serialVersionUID的作用:如果类结构变化(新增字段),反序列化时会校验UID,不一致会抛InvalidClassException。因此建议显式声明一个serialVersionUID,例如private static final long serialVersionUID = 1L;。

还有一个坑:static变量不会被序列化,transient修饰的变量也不会。为什么?因为static属于类,transient表示不参与序列化。如果你想忽略某个字段,就用transient修饰。基础阶段可以做个小实验:写一个User对象,序列化到文件再反序列化回来,看看字段的变化情况。

8. JDK环境、编译与运行:这些“基础之外的基础”别栽跟头

8.1 JDK安装与环境变量:别让第一步卡住你

新手最容易在环境配置上浪费时间。我建议直接下载官方JDK(比如当前很多项目还在用JDK 8,新项目向JDK 11、17升级),安装后配置JAVA_HOME、PATH。这里说一个细节:JAVA_HOME要指向JDK安装目录,而不是bin目录;PATH要添加%JAVA_HOME%\bin,这样才能在命令行里直接用java和javac。

配置完成后,打开命令行输入java -version验证。如果显示“不是内部或外部命令”,多半是PATH没配好,或者修改环境变量后没有重新打开命令行。还有一个小坑:Windows下安装JDK时注意32位和64位别选错,只要系统是64位的,就下载64位JDK,否则启动某些IDE会报错。

8.2 编译和运行:用javac和java理解class与字节码

好多人在IDE里点一下绿色三角就跑起来了,根本不了解背后的javac和java。基础阶段,我强烈建议你手动编译一次:

javac Hello.java java Hello

javac会把.java文件编译成.class字节码文件,java命令负责启动JVM并解释执行字节码。如果你把Hello.class删了再运行,会报NoClassDefFoundError。这个体验能让你理解“Java是半编译半解释的语言”,对后面理解JVM、JIT优化都有帮助。

如果你用的是JDK 11及以上,可以直接java Hello.java运行单文件源码,这是Java 11的“单文件源码启动”特性,适合写小工具,但了解即可,正式项目还是用构建工具。

8.3 构建工具和IDE:Maven / Gradle是基础的一部分

虽然构建工具不算语法,但你做Java项目绕不开。现在的Java基础学习路线,至少要会Maven的基本用法:pom.xml里声明依赖、mvn clean package打出可执行jar包。Gradle在新项目里也很多,但原理上都是“依赖管理+构建生命周期”。

如果你连mvn -v都跑不出来,面试说“熟悉Java基础”会很虚。我的建议是:装一个IDEA社区版(完全够用)或者Eclipse,但一定要学会命令行编译和Maven命令,这样才能在无IDE环境里也能干活。另外,理解classpath的作用:java -cp my.jar com.example.Main,这样你才知道为什么依赖需要打包进jar里,不然运行会报ClassNotFoundException。

9. 常见问题与排查技巧实录:我踩过的坑,希望你绕过

9.1 空指针到底怎么根治?

空指针(NullPointerException)是Java基础阶段遇到最多的异常。我给的排查三步:第一,看异常栈,定位到行号;第二,检查那行代码里哪些对象可能为null;第三,用Objects.requireNonNull或者防御性判断。

但更根本的做法是“不要让null传递”。方法参数如果要求非空,就用@NonNull标记或者直接Objects.requireNonNull;返回值如果是集合,尽量返回空集合而不是null,比如Collections.emptyList()。这样上游就不需要每次判空。这个习惯能极大地降低空指针率。

另外,Java 8的Optional是个好工具,但它不是用来做字段类型的,而是作为返回值暗示“可能为空”。比如Optional<User> findById(Long id)。用法上注意别把Optional当参数传,那会让代码更啰嗦。

9.2 集合并发修改异常:ConcurrentModificationException怎么处理?

我在3.4节提过,遍历时删除元素会抛这个异常。除了用Iterator.remove,还可以用removeIf:

list.removeIf(s -> s.equals("a"));

这个API底层就是迭代器删除,很干净。或者,你创建一个新集合,收集要删除的元素,遍历结束后再批量删除。你在面试时可以主动提到这几种方式,会显得经验丰富。

9.3 堆内存溢出和栈溢出:从错误信息反推问题

java.lang.OutOfMemoryError: Java heap space表示堆内存不够。排查方式:先看是不是有对象一直被引用没释放,用jmap或VisualVM导出堆转储分析。如果是StackOverflowError,多半是递归没有终止条件,比如在写快速排序时递归边界少写了low >= high,就会无限递归。

基础阶段不需要精通JVM调优,但至少要能看懂这两个错误的区别:堆是对象存储区,栈是方法调用区。方法调用层级太深,栈空间不够就溢出;对象太多且回收不掉,堆空间不够就堆溢出。

9.4 编译报错:如何从“看不懂”到“秒懂”

新手经常被编译报错吓到,其实编译错误是最友好的,因为它发生在编译期,JVM还没有运行你的代码,错误信息通常直接指出了行号。关键是学会看第一行错误:cannot find symbol表示你用了没定义的变量或方法;incompatible types表示类型不匹配;missing return statement说明方法缺少返回。我见过很多人看到一长串报错就慌,其实只要看第一个^符号指向的位置,就能定位问题。

解决编译错误的另一个技巧是“最小化排查”:把你怀疑出错的代码注释掉一半,分而治之。别人不告诉你,你可能要花很久。但这确实是基础阶段最值得练的能力——不是背语法,而是学会读错误信息。

10. 给零基础的学习路线与实操建议

10.1 阶段式学习方案:别想一口吃成胖子

如果你零基础,我给一个曾被多人验证过的路线:

阶段一:语法基础(2-3周)。学数据类型、运算符、流程控制、数组、方法。每天写代码至少2小时,敲熟循环和条件判断。

阶段二:面向对象(3-4周)。学类与对象、封装继承多态、接口、异常、常用类(String、包装类)。这个阶段你要做一个小系统,比如“学生管理系统”,用面向对象思维设计类。

阶段三:集合与IO(2-3周)。学集合框架、文件读写。练习对集合进行增删改查,把数据写到文件再读出来。

阶段四:JVM基础与并发入门(2周)。学内存模型、GC概念、Thread、synchronized、volatile。不需要深究,但要能讲明白基本概念。

阶段五:工程化工具(1-2周)。学Maven、Git、IDEA快捷键,然后开始接触Spring Boot。如果你按这个节奏学,两个月后就可以做简单的Web项目了。

10.2 刷题和手写结合:面试前的高效复习法

基础面试题普遍集中在这几块,我按优先级列一下:

  • 字符串相关(String不可变性、StringBuilder vs StringBuffer、String vs new String)
  • 集合源码(ArrayList扩容、HashMap原理、LinkedHashMap的LRU实现思路)
  • 并发基础(synchronized锁升级、volatile、ThreadLocal、线程池参数)
  • JVM基础(内存分区、类加载过程、GC算法)
  • 设计模式(单例、工厂、代理,能手写懒汉式和DCL单例是加分项)

刷题时注意,不要只看答案,要自己默写代码。特别是单例模式、快速排序、二分查找、反转链表这几个,几乎是必考手写。

我说一个我自己的土方法:把重要的代码段用手机拍下来或存到云笔记里,等地铁时就在脑子里过流程,比如“HashMap put流程:计算hash->定位桶->链表或树->判断扩容”。这种“脑内模拟”比反复读书记得牢。

10.3 资源选择原则:官方文档永远第一

网上Java学习资料浩如烟海,但我的原则是:遇到语法问题,优先查官方文档(Java SE Documentation)和源码;遇到设计问题,看《Effective Java》和《Java并发编程实战》;遇到面试问题,看靠谱的面试题库,但一定要亲手跑代码验证。很多所谓的“面试题解析”本身就有错误,你不验证就会背错。

如果你在学集合,就直接打开JDK源码里的HashMap.java,一边看注释一边看代码,比自己找二手博客强一百倍。基础阶段培养“阅读源码”的能力,到后面学框架源码时受益匪浅。

写在最后:基础不是用来背的,是用来用的

我接触Java这些年,最深刻的一个体会是:基础决定你的“天花板高度”。框架千变万化,但底层永远逃不出集合、线程、IO、内存这些老伙计。你基础扎实,学任何新框架都很快,因为你看到的是“它不过是封装了某个机制”;你基础不牢,只能靠背API过日子,换一个版本就抓瞎。

个人经验再补一条:初学者最容易犯的错是“只读不写”。看十遍HashMap源码,不如自己手写一个简化版的HashMap(数组+链表+取模),哪怕只实现put和get,写完你对哈希的理解会上升一个层次。学多线程也一样,别嫌写demo无聊,跑出问题再解决,比任何书都管用。

Java基础必知的内容讲到这里,剩下的就交给你的键盘了。敲起来吧,坑踩完了,基础也就真正进脑了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询