
说个现象最近帮几个刚入行的朋友看代码发现他们遇到最多的问题不是高并发、不是分布式而是最基础的Java语法、数据结构和JDK底层行为。有人为了准备面试一上来就背八股文背HashMap源码、背JVM调优结果连Integer在-128到127之间有缓存、数组下标从0开始这种基础问题都会栽跟头。这篇Java基础核心知识点一我打算把学习Java和准备面试时最核心、最容易被忽略的基础知识重新捋一遍覆盖数据类型、面向对象、字符串处理、数组与排序、集合容器这几个方向。不管你是准备蓝桥杯、应付学校考试还是正在刷Java面试题这篇都值得收藏慢慢看。后续我会继续写第二篇、第三篇把基础这块彻底讲透。1. 数据类型基础先搞清楚int占几个字节再谈别的1.1 八种基本类型要背到什么程度Java有8种基本数据类型byte、short、int、long、float、double、char、boolean。很多人觉得背这个没意义但实际开发中类型选错是会出线上事故的。比如用一个int接收金额万一金额超过21亿就直接溢出了用float做精确计算小数点后的误差会累积到无法接受。类型占用位数默认值取值范围byte80-128 ~ 127short160-32768 ~ 32767int320-2147483648 ~ 2147483647long640L-9223372036854775808 ~ 9223372036854775807float320.0f约 ±3.4E38double640.0d约 ±1.8E308char16\u00000 ~ 65535boolean未明确falsetrue / false这里有个细节值得说char在Java里是16位无符号整数所以它可以存储一个Unicode字符。这一点和C语言完全不同C的char是8位。另外面试官很喜欢问float和double为什么不能直接比较——因为浮点数是二进制近似表示的0.1在计算机里其实是无限循环小数所以金额计算、精度要求高的场景一定要用BigDecimal。再说一个日常写代码常见的坑计算平均成绩。很多人直接写int avg sum / count两个整数相除结果直接截断成整数。热搜里有个java怎么计算输出平均成绩考的就是这个点。正确的做法是先把其中一个操作数转成double比如double avg (double) sum / count或者在定义sum时就用double。类似地int a 5 / 2结果永远是2不是2.5。1.2 自动拆装箱与Integer缓存一个让无数人翻车的细节Java 5引入了自动拆装箱基本类型和包装类型可以互相转换这个语法糖用起来很爽但也埋了不少坑。先看这段代码你能说出结果吗Integer a 127; Integer b 127; Integer c 128; Integer d 128; System.out.println(a b); // true System.out.println(c d); // false原因在于Integer内部维护了一个IntegerCache缓存范围是-128到127。用Integer.valueOf()取值时如果在这个范围内直接返回缓存对象所以a和b指向同一个对象。超出范围就会new Integer()所以c和d是两个不同对象比较自然是false。这个知识点几乎是Java面试题的必考点。我建议你养成一个习惯包装类型之间的比较一律用equals()别用。你可能会说我开发时没碰到啊那是因为你恰好都在缓存范围内或者用了equals。一旦数据超过127线上就会出现莫名其妙的bug排查起来很费劲。自动拆箱还有另一个坑包装类型参与运算时会自动拆箱但如果它是null拆箱时就会抛出NullPointerException。比如Integer count null; int x count 1;这行直接崩。所以数据库查询结果转包装类型时一定要判空。1.3 标识符命名规则从一次编译报错说起群里有个朋友写代码变量名叫1stName编译直接报错他半天没反应过来。Java的标识符规则其实很清晰只能由字母、数字、下划线_、美元符号$组成不能以数字开头不能是Java关键字比如class、int、if这些理论上可以用中文做变量名但强烈不建议容易造成编码混乱和团队协作问题。命名规范方面业界通用的是驼峰命名法类名用大驼峰StudentInfo变量名和方法名用小驼峰studentName、getStudentName常量全大写加下划线MAX_SIZE。这个不仅是给面试官看的更是给你自己看的——三个月后你回来看自己写的代码规范的命名能帮你快速回忆起结构。这里还要提一个细节有些框架在做参数映射、反射操作时会依赖字段名命名不规范可能导致运行时找不到属性。我在实际项目中就碰到过因为字段名用了拼音缩写结果序列化出来的JSON字段名完全没法看前端同事差点提刀来找我。2. 面向对象三件套封装、继承、多态到底解决了什么问题2.1 封装不是写个getter/setter就完事了教科书上说封装就是把属性私有化通过公有方法访问。但很多人只记住了形式没理解为什么。我举个反例public class Account { public int balance; }如果balance是public的外部代码可以直接写成account.balance -1000业务规则完全失控。更隐蔽的是未来你想在余额变化时加一条流水记录、发一个通知根本无从下手因为所有代码都在直接操作字段。正确的封装是用private把字段藏起来通过方法暴露操作在方法里加约束public class Account { private int balance; public void deposit(int amount) { if (amount 0) { throw new IllegalArgumentException(存款金额必须大于0); } balance amount; } public void withdraw(int amount) { if (amount balance) { throw new IllegalArgumentException(余额不足); } balance - amount; } }这才是封装的意义不是限制你访问而是保证数据永远是合法的。面试如果被问到封装别只回答private getter/setter要说出通过方法对字段的读写增加业务校验逻辑这层理解。2.2 继承什么时候用什么时候千万别用Java是单继承一个类只能有一个父类。继承描述的是is-a关系比如Cat extends Animal猫是一种动物这在语义上是通的。继承有两个最常见的坑。第一个坑构造器链。子类构造器会隐式调用父类的无参构造器如果父类没有无参构造器子类编译都过不去。正确做法是在子类构造器第一行显式调用super(参数)。第二个坑重写方法和父类方法的约束。重写时访问权限不能比父类更严格比如父类方法是public子类就不能写成private。返回值类型可以是父类返回值类型的子类型协变返回。抛出的异常不能比父类方法更宽泛。在实际项目里我更推荐组合优先于继承这个原则。继承是强耦合的父类一旦改动所有子类都可能受影响。而组合是has-a关系比如Car类里放一个Engine对象想换引擎逻辑时只需要替换Engine实现不影响Car本身。2.3 多态面向接口编程的价值多态分为编译期多态方法重载和运行期多态方法重写。重载看的是参数列表重写看的是继承体系中的方法覆盖。面试喜欢问重载和重写的区别其实核心就一句话重载是同一个类里方法名相同、参数不同重写是子类重新实现父类的方法方法签名必须一致。多态最大的价值在于面向接口编程而不是面向实现编程。比如你定义一个排序接口public interface Sorter { void sort(int[] arr); } public class BubbleSorter implements Sorter { Override public void sort(int[] arr) { // 冒泡排序实现 } } // 调用方只依赖接口 Sorter sorter new BubbleSorter(); sorter.sort(data);将来你想换成快速排序只需要新建一个QuickSorter实现Sorter接口调用方的代码一行都不用改。这就是多态带来的扩展性。很多框架的插件机制、策略模式本质都是这套思路。3. 字符串处理从判断非字母数字看String的常见坑3.1 判断字符串中是否包含非字母数字有几种写法热搜词里有一条java 判断字符串中是否不是字母和数字这个需求其实很常见校验密码不能包含特殊字符、过滤输入内容等。我给出三种写法从最推荐到最不推荐。第一种遍历加Character.isLetterOrDigit。public static boolean containsOnlyLetterAndDigit(String str) { if (str null || str.isEmpty()) { return false; } for (int i 0; i str.length(); i) { if (!Character.isLetterOrDigit(str.charAt(i))) { return false; } } return true; }这个方法时间复杂度O(n)逻辑清晰推荐优先使用。注意Character.isLetterOrDigit对中文字符也返回true如果你只想要ASCII字母和数字需要自己写范围判断private static boolean isAsciiLetterOrDigit(char c) { return (c a c z) || (c A c Z) || (c 0 c 9); }第二种正则表达式。boolean isValid str.matches([a-zA-Z0-9]);正则写法简洁但String.matches()每次都会编译正则表达式如果在一个大循环里频繁调用性能会受影响。而且字符串为空时matches会返回false需要结合业务判断。第三种Java 8 Stream写法。boolean isValid str.chars().allMatch(Character::isLetterOrDigit);这种适合函数式风格但可读性一般初学者不推荐。3.2 字符串常量池、和equals老生常谈但总是有人错字符串内容的比较必须用equals()不能用这个道理几乎所有Java程序员都知道但知道和做到是两回事。其原因在于字符串常量池String s1 abc; String s2 abc; String s3 new String(abc); System.out.println(s1 s2); // true字面量在常量池中复用 System.out.println(s1 s3); // falsenew出来的是堆上新对象 System.out.println(s1.equals(s3)); // true内容相同还有一个intern()方法可以把堆上的字符串对象丢进常量池并返回池中的引用。这个在面试里经常被拿出来考但实际开发中很少用到了解即可。String是不可变类每一次拼接操作都会产生新的字符串对象。循环拼接时会产生大量中间对象给GC带来压力。正确做法是用StringBuilder.append()StringBuilder sb new StringBuilder(); for (int i 0; i 1000; i) { sb.append(i).append(,); } String result sb.toString();这里有个小细节循环前先预估字符串大小给StringBuilder指定初始容量能减少扩容次数。比如上面这个例子如果大约需要5000个字符容量直接写new StringBuilder(5000)。4. 数组越界与冒泡排序基础算法中最该小心的边界问题4.1 ArrayIndexOutOfBoundsException是怎么来的数组下标从0开始长度是arr.length有效下标范围是0到arr.length - 1。这个知识点背起来简单写代码时却总有人踩坑。最常见的错误是循环条件写成i arr.lengthint[] arr {1, 2, 3}; for (int i 0; i arr.length; i) { System.out.println(arr[i]); // 当 i 3 时抛出 ArrayIndexOutOfBoundsException }排查这类异常有个经验先看异常栈里的行号定位到具体代码行再检查for循环的边界条件。大多数情况不是逻辑复杂而是少写了一个减号或者多写了一个等号。还有一种隐蔽的越界二维数组在遍历时没有处理好内层长度arr[i][j]中的j可能超过arr[i].length。实际开发中从文件或数据库读数据转成数组时也要特别注意索引和长度不匹配的问题。4.2 从零推导冒泡排序为什么要减i冒泡排序是入门第一课也是蓝桥杯、面试题里反复出现的基础。它的原理是每一轮从头开始相邻元素两两比较大的往后移就像气泡一样慢慢浮到末尾。标准实现public static void bubbleSort(int[] arr) { int n arr.length; // 外层循环控制比较轮数最多 n-1 轮 for (int i 0; i n - 1; i) { // 内层循环控制每轮比较的次数 // 每一轮结束后数组末尾的 i 个元素已经排好无需再比较 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; } } } }很多人不理解内层循环为什么要n - 1 - i。其实很简单第一轮比较n-1次后最大的元素被移到了最后一位第二轮只需要比较前n-2个元素因为最后一位已经有序。如果不减i程序不会报错但会做大量无用比较而且可能导致越界。冒泡排序的时间复杂度是O(n²)空间复杂度O(1)是稳定排序。如果数组本身已经有序我们仍然会走完所有轮次。可以加一个swapped标记来优化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; // 这一轮没有发生交换说明已经有序直接退出 } } }4.3 排序中的几个常见错误我见过不少人在写冒泡排序时翻车总结下来就几类内层循环的条件写成j n当j n - 1时访问arr[j 1]必然越界比较符号写反if (arr[j] arr[j 1])结果排成了降序还一脸疑惑打印数组时直接System.out.println(arr)输出的是[I1b6d3586这种内存地址应该用Arrays.toString(arr)交换变量时忘了用临时变量或者写成了arr[j] arr[j 1]; arr[j 1] arr[j];两个值直接变成同一个。学排序的时候我建议你亲手把每一轮的结果打印出来写一个Arrays.toString()的日志然后对照推导过程看。这样对边界条件的理解会比死记硬背深刻得多。5. 集合容器选型ArrayList与HashMap的底层逻辑5.1 ArrayList扩容机制你知道它什么时候扩容吗ArrayList底层是Object[]数组。JDK 8中new ArrayList()不会立即创建数组而是等到第一次添加元素时才初始化初始容量是10。当元素数量超过数组容量时会进行扩容新容量大约是旧容量的1.5倍oldCapacity (oldCapacity 1)然后把旧数组元素复制到新数组。这个扩容过程很消耗性能。如果你事先知道大概会存储多少元素一定要用new ArrayList(capacity)初始化容量避免频繁扩容。举个例子你要从数据库查出1万条记录封装成对象再放进List如果不指定容量中间可能要扩容十几次每次都是数组复制白白浪费时间和内存。ArrayList和LinkedList的对比是经典八股文。LinkedList底层是双向链表理论上中间插入删除更快、不需要扩容。但实际开发中绝大多数场景用ArrayList就够了因为随机访问get(index)ArrayList是O(1)LinkedList是O(n)CPU缓存局部性方面数组的连续内存对缓存更友好现代JVM对数组的优化非常成熟链表反而因为对象分散导致内存碎片化。所以在没有明确的频繁中间插入删除需求时别迷信LinkedList。5.2 HashMap数组加链表加红黑树的设计逻辑HashMap是面试问得最多的容器没有之一。JDK 8之后的HashMap底层是数组 链表 红黑树。put操作的简化流程对key调用hashCode()再经过扰动处理得出哈希值用哈希值定位数组桶下标(n - 1) hash前提是数组长度必须是2的幂如果该位置为空直接放入如果不为空遍历链表或红黑树找到相同key则覆盖值否则追加节点链表长度超过8并且数组长度达到64链表转红黑树元素数量超过负载因子默认0.75乘以数组长度触发扩容容量翻倍。这里有两个面试高频细节。第一为什么数组长度必须是2的幂因为(n - 1) hash相当于hash % n但位运算速度更快而且当n是2的幂时n - 1的二进制全是1能更均匀地分布哈希结果。第二重写equals()必须重写hashCode()。这是Java规范也是无数人踩坑的点。如果你把自定义对象当作HashMap的key只重写了equals没重写hashCode那么hashCode默认是对象内存地址。即使两个对象逻辑上相等hashCode也不同HashMap会认为它们是两个不同的key导致存进去取不出来。5.3 遍历时删除元素为什么会抛ConcurrentModificationException在用for-each遍历ArrayList或HashMap时如果在循环体里调用remove()通常会抛出ConcurrentModificationException。这是fail-fast机制在起作用迭代器内部维护了一个modCount每次修改集合都会让modCount加一当迭代器发现modCount和自己预期不一致时立即抛出异常。正确删除方式是使用迭代器的remove()方法IteratorString iterator list.iterator(); while (iterator.hasNext()) { String s iterator.next(); if (bad.equals(s)) { iterator.remove(); } }或者直接用Java 8的removeIflist.removeIf(s - bad.equals(s));如果涉及多线程并发修改优先考虑CopyOnWriteArrayList或ConcurrentHashMap这些并发容器而不是用Collections.synchronizedList包一层就完事。后者只是把方法加锁但迭代时仍然不安全。6. 环境配置、调试方法与学习顺序给新手的实在建议6.1 JAVA_HOME配置大部分环境问题都出在这里很多人卡在javac能用java不能用或者两个JDK版本冲突上。其实环境变量配置的核心就三个JAVA_HOME指向JDK安装目录PATH里加上%JAVA_HOME%\binWindows或$JAVA_HOME/binLinux/macOS不要同时配置多个JDK路径在PATH里容易串版本。Windows下还有一个常见坑装了多个JDK但PATH里系统变量和用户变量的顺序不对导致命令行里java -version显示的是旧版本。解决办法是把新JDK的路径放在最前面或者干脆把旧版本的JAVA_HOME删掉。Linux下推荐用update-alternatives管理JDK版本或者直接用SDKMAN这类工具切换版本很顺手。6.2 调试先学会看异常栈再谈输出打印新手调试最大的问题是不知道从哪里开始。我的建议是先学会完整地读异常栈。Exception in thread main java.lang.NullPointerException at com.example.UserService.getUserName(UserService.java:35) at com.example.Main.main(Main.java:12)从最上面的at行开始看那是异常抛出的原始位置。如果看到Caused by:一定要顺着往下看那是底层真正的原因。我曾经排查过一个线上问题异常栈写了十几行很多人盯着顶部的业务代码看半天找不到原因其实Caused by那一行明确写着是数据库连接超时。调试手段优先级断点调试 日志输出 随便加System.out.println。现在主流IDE的断点调试功能非常强大可以查看变量值、表达式、调用栈建议尽早学会不要只会打印。6.3 学习路线别一上来就啃JVM源码结合热搜词里出现频率很高的java学习路线java自学路线图和java八股文我想专门说几句。建议的学习顺序是基础语法变量、数据类型、运算符、流程控制→ 面向对象封装继承多态、接口→ 常用类库String、集合框架、日期时间→ 异常处理与调试 → I/O流 → 泛型与反射 → 多线程与并发 → JVM内存与垃圾回收 → 框架Spring Boot等。刷题可以从蓝桥杯、力扣的简单题开始。蓝桥杯的Java组题目很务实经常考数字处理、排序、字符串操作正好能把你前面学的基础知识用起来。面试前再看八股文才有意义因为它是对已有知识的总结而不是让你死记硬背的答案。我在带新人时发现很多人学不下去的原因不是不努力而是太焦虑觉得要学的东西太多。实际上Java基础核心知识点就这么多一篇篇拆开学每个知识点配合代码练习三个月完全可以建立起一个很扎实的地基。这篇是一后续我还会写集合的进阶用法、异常与并发、I/O与网络编程这些内容。先把基础啃下来后面的路会越走越顺。