ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java基础知识点复习笔记(上):数据类型、面向对象、字符串、集合与异常

Java基础知识点复习笔记(上):数据类型、面向对象、字符串、集合与异常 “java基础知识点复习笔记上”——看到这个标题我第一反应是又要准备跳槽或者准备校招了吧。说实话这些年我面试过不少人也被别人面试过一个Java开发者基础扎不扎实往往聊不到十分钟就能看出来。基础这东西平时写业务代码感觉不到它的存在但一到面试、一到排查线上问题、一到做代码重构它就会跳出来考验你。这份笔记我整理了很久定位是给两类人看的一类是准备Java面试的开发者需要把零散的知识点系统过一遍另一类是工作了两三年、平时CRUD写得多但想查漏补缺的朋友。内容不会像教科书那样面面俱到而是把高频考点、容易踩坑的点、面试官真正想听的点拎出来配合代码示例和实战经验来讲。既然是“上”篇我先聚焦最核心的地基部分数据类型、面向对象、字符串、集合框架、异常处理。这些都是Java体系里最基础也最常考的内容把这些吃透了再往JVM、并发、Spring那些深水区走才不会心虚。1. 数据类型基础中的基础却藏着最多的坑1.1 八大基本类型与包装类你真的分清楚了吗Java的基础数据类型是八种byte、short、int、long、float、double、char、boolean。这是面试开场最常见的问题但很多人答得并不完整只会背“int 4字节、long 8字节”一旦问到float和double为什么不能直接用比较就卡住了。先给一张我复习时自己整理的速查表类型字节数默认值取值范围大致应用场景byte10-128 ~ 127二进制流处理、节约内存的数组short20-32768 ~ 32767较少使用兼容老协议时见过int40约±21亿最常用的整数类型long80L很大时间戳、ID、大数值计算float40.0f约±3.4E38科学计算精度有限double80.0d约±1.7E308默认浮点类型char2\u00000 ~ 65535字符处理boolean未明确定义falsetrue/false逻辑判断注意boolean的字节数Java虚拟机规范里没有明确规定实际实现中JVM通常用int代替boolean所以一个boolean在数组中可能占1字节单独声明时可能占4字节。这个细节面试官偶尔会挖能答出来会加分。包装类是另一个高频考点重点是自动装箱与拆箱的底层实现。Integer i 100这行代码编译后实际是Integer.valueOf(100)而int j i编译后是i.intValue()。所以每一种包装类都对应一个静态工厂方法和一个xxxValue方法这是基础中的基础必须记牢。int的默认值是0但Integer的默认值是null我见过太多人在写实体类时用int做字段类型数据库一查出来没有值直接NPE这是真实项目里最常见的低级错误。1.2 Integer缓存、浮点精度与陷阱Integer缓存是面试题里的常青树。Integer.valueOf()方法在 -128 到 127 之间会返回缓存的同一个对象超出这个范围才new新对象。所以Integer a 100; Integer b 100; a b返回true而把100换成128就返回false。这个坑我在代码评审里见到过不止一次有人用比较两个包装类小数值没问题一上线数据大了就开始出bug。解决办法很简单两个包装类比较一律用equals()或者用Objects.equals()。但这里还有个新坑就是包装类的equals对类型敏感new Integer(100).equals(100L)返回false因为一边是Integer一边是Long。所以如果要比较不同类型数值建议先把它们转成同一个类型再比较或者用BigDecimal。浮点精度问题是另一个必须掌握的考点。0.1 0.2 0.3在Java里返回false因为浮点数在二进制中无法精确表示这是计算机组成原理决定的。面试官通常在这个问题后面会追问那怎么办标准答案是涉及金额计算时用BigDecimal而且BigDecimal构造时必须传字符串否则new BigDecimal(0.1)得到的仍然是不精确的值。我自己写金融相关代码时会统一封装一个金额工具类内部用BigDecimal任何金额计算都不允许用double直接做。2. 面向对象从概念背诵到真正理解2.1 封装、继承、多态面试官真正想听的是什么面向对象三大特性几乎每个Java面试都会问。但大部分人的回答就是背定义“封装就是把属性私有化继承就是子类继承父类多态就是一个接口多种实现。”这些没错但不够。面试官问三大特性其实是想听你对“设计”的理解。封装的核心不是private修饰符而是“隐藏实现细节暴露稳定接口”。我复盘自己的开发经验封装做得好不好直接表现在需求变更时的改动量上。比如一个订单金额计算逻辑如果只有getOrderAmount()这个方法对外暴露里面怎么调整折扣、怎么计算税费调用方根本不用关心这就是封装的意义。继承在面试里的重点往往会引导到“继承与组合怎么选”这个问题上。继承是Is-A关系组合是Has-A关系。我见过很多代码滥用继承比如为了复用两个方法就extends一个类结果父类一改动子类莫名其妙出bug。实际项目里我倾向优先使用组合因为组合更灵活不会打破封装。但继承在框架代码里确实随处可见比如Spring的Service类通常继承一个抽象基类Template Method模式就是靠继承实现的。面试时能讲出这个权衡比单纯背概念要加分得多。多态是我认为三大特性中最核心的。Java的多态体现在方法重载和方法重写但底层原理是动态绑定——虚拟机在运行时根据对象的实际类型来调用方法。这里面试官常会追问重载和重写的区别重载是编译期决定的看的是引用类型和参数列表重写是运行期决定的看的是实际对象类型。所以Father f new Son(); f.method();调用的到底是Father的method还是Son的method答案是Son的如果Son重写了method的话因为JVM在运行时才做动态分派。这个考点我面试别人时几乎必问答得清楚的人说明对多态的理解是到位的。2.2 抽象类与接口Java 8之后界限已经模糊了抽象类和接口的区别是经典面试题但很多人的答案是老版本Java的答案。Java 8之后接口可以有default方法和static方法Java 9之后接口还可以有private方法再加上接口的字段只能是public static final常量抽象类可以有各种成员变量——两者的边界确实模糊了但设计语义仍然不同。我的理解是抽象类描述的是“是什么”接口描述的是“能做什么”。比如AbstractList和List接口的关系就很典型。AbstractList是抽象类它把List接口的部分方法实现了留给子类去扩展而List接口定义了列表该有的行为契约。一个类只能继承一个抽象类但可以实现多个接口这是Java设计上的硬性约束。面试时如果你能主动提到Java 8的default方法带来的一个经典问题——菱形继承会让面试官觉得你是有思考的。一个类实现了两个接口两个接口都有同名的default方法编译会报错必须重写这个方法并手动指定调用哪个接口的default方法。代码是这样interface A { default void hello() { System.out.println(A.hello); } } interface B { default void hello() { System.out.println(B.hello); } } class C implements A, B { Override public void hello() { A.super.hello(); // 指定调用A的默认方法 } }这个知识点不常考但能答出来会明显提升面试的印象分。我做技术方案评审时也会用这个案例提醒团队接口新增default方法要考虑兼容性不能想加就加。2.3 重载与重写别把这两个概念搞混了重载和重写的区别几乎每次笔试都会出选择题但很多人还是会踩坑。我总结一句话重载是同一个类里方法名相同、参数列表不同重写是子类方法覆盖父类方法方法签名必须一致返回类型可以是协变类型访问修饰符不能比父类更严格抛出的异常不能比父类更宽泛。实际项目里重写的坑点我遇到过一个很经典的子类重写方法时把参数类型改了比如父类是method(String s)子类写成了method(Object o)这其实变成了重载而不是重写加上Override注解编译器立刻就会报错。所以我会跟团队说重写方法必须加Override注解这不是可选项是强制规范。重载的另一个注意点是自动装箱的参与method(int i)和method(Integer i)同时存在时调用method(1)会优先匹配method(int)因为基本类型的匹配优先级更高不会触发装箱操作。而method(null)在这种情况下会报歧义错误因为null既可以装箱成Integer也可以匹配Object等类型编译器无法判断。这个考点我在字节的笔试里见过属于没写过就容易栽的题。3. 字符串Java中最特殊的一个类3.1 String为什么是不可变的String的不可变性immutability是面试高频题也是理解JVM字符串常量池的前提。String类被final修饰字符数组也是final的所有修改字符串的方法返回的是新对象原对象不变。这个设计不是随意的背后有几个原因一是字符串常量池的复用。如果字符串可变那么两个指向同一个字符串常量的引用一个把内容改了另一个也会跟着变这将摧毁常量池的安全性和缓存意义。二是安全性String在Java里被广泛用作参数、类名、URL等不可变可以避免恶意修改。三是线程安全不可变对象天然线程安全不需要额外加锁。实际开发中我遇到过因为拼接字符串导致的内存问题。在一个循环里用做字符串拼接循环几千次每次都会在堆里产生新的字符串对象GC压力很大。正确的做法是用StringBuilder或者StringBuffer。面试官可能会问这两者的区别StringBuilder线程不安全StringBuffer线程安全方法加了synchronized单线程场景下StringBuilder性能更好。这里给个我自己的实用建议不要在方法内使用StringBuffer因为方法内没有多线程竞争StringBuffer的同步开销毫无意义但如果字符串是类的成员变量且可能被多个线程操作那就必须用StringBuffer或者加锁。这个细节面试时主动提出来面试官会认为你有真实的并发编程经验。3.2 常量池与intern()追根溯源的一道好题常量池相关最经典的一道面试题是String s new String(abc)创建了几个对象答案是可能两个一个是常量池里的abc如果之前没有的话一个是堆里new出来的对象。如果常量池里已经有abc则只创建一个。这个题考察的是JVM内存结构和字符串驻留机制回答时最好把两种情况分开说清楚。intern()方法的作用是把字符串放入常量池如果常量池已有相同内容的字符串则返回池中的引用。实际操作中我很少主动调intern()但有个场景很有用在大量重复字符串的场景下比如从数据库查出来一万条数据每条都有相同的状态字段用intern()可以极大降低内存占用。但要小心JDK 7之前intern的字符串放在永久代容易OOMJDK 7之后移到了堆里风险低了但也不是完全没风险。面试能说到这里基本就很完整了。关于字符串比较的区分我还要强调一点abc.equals(s)和s.equals(abc)在s可能为null时前者更安全因为常量字符串的equals不会抛NPE。我见过项目里因为s.equals(xx)这种写法在s为null时直接抛异常后来排查发现是数据源里出现了一条脏数据。代码规范上我推荐字符串比较统一写成常量在前或者直接用Objects.equals(s, abc)。3.3 字符串拼接编译器的优化与反例字符串拼接还有一个隐藏考点编译器对的优化。Java 8及之前字符串常量拼接如a b在编译期就合并成ab了所以a b ab是true。但涉及变量的拼接str b编译后实际上是new StringBuilder().append(str).append(b).toString()。Java 9之后引入了invokedynamic和StringConcatFactory优化方式变了但结果仍然是生成新字符串。一个常见的错误认知是循环里用拼接和StringBuilder性能差不多因为编译器会优化。实际上在循环体内每次都会创建新的StringBuilder性能反而更差。我在一次性能排查中发现一个字符串拼接逻辑在循环里执行了五万次耗时直接拖慢了接口响应改成循环外创建StringBuilder后耗时几乎降为原来的三分之一。所以结论是循环内拼接字符串一定要手动用StringBuilder。4. 集合框架核心高频考点的聚集地4.1 集合体系结构先建立整体认知Java集合框架从顶层看分为两大体系Collection存放单列元素和Map存放键值对。Collection下面又分为List、Set、Queue三大接口。List是允许重复、有序的集合Set是不允许重复的集合Queue是队列。Map则是独立的体系每个元素是一个键值对。面试官一般会先从宏观问起ArrayList和LinkedList的区别。这个题的基础答案是ArrayList底层是数组随机访问快、插入删除慢LinkedList底层是双向链表插入删除快、随机访问慢。但深入一点的考法是具体在什么场景下用哪个我的实战经验是开发中90%以上的情况都选ArrayList因为遍历、索引的需求占绝大多数LinkedList的插入删除快是有前提条件的它在中间插入时需要先遍历找到位置复杂度是O(n)真正快的只有头尾操作。这里我给一个面试加分点LinkedList在Java 6之前实现的是纯双向循环链表所以addFirst和addLast都是O(1)Java 7之后去掉了循环结构变成普通双向链表但对实际使用影响不大。能答出这个版本差异的候选人说明是真的看过源码的。4.2 HashMap原理面试必问的重头戏HashMap是Java集合里最核心的考点几乎每场面试必问。面试官会连环追问底层数据结构是什么put的流程是什么扩容机制是什么为什么用红黑树我按这个顺序把核心逻辑梳理一遍。HashMap在JDK 1.8之后的底层是数组链表红黑树。put一个键值对时先通过hash(key)算出哈希值再用(n - 1) hash定位到数组下标这里n是数组长度这个位运算等价于取模但效率更高。如果该位置没有元素直接放入如果有元素比较key是否相同相同则覆盖不同则插入链表尾部尾插法。当链表长度超过8时链表会转化为红黑树这是为了把极端情况下的查询复杂度从O(n)降到O(log n)。扩容是另一个高频考点。HashMap的默认初始容量是16负载因子是0.75当元素个数超过容量 * 负载因子时容量翻倍为原来的两倍。为什么负载因子是0.75这是个权衡太小了浪费空间太大了增加哈希冲突概率。而且HashMap的容量要求是2的幂次方因为只有容量是2的幂时(n - 1) hash才能均匀分布。关于HashMap为什么用红黑树而不是平衡二叉树我补充一个理解红黑树不是严格平衡的它只保证从根到叶子的最长路径不超过最短路径的两倍这样插入和删除的代价低查询效率也能接受。AVL树是严格平衡的插入删除时需要更多旋转操作实际性能反而不好。这个细节能答出来说明你真的研究过数据结构。4.3 线程安全集合fail-fast与并发容器的选择题集合相关还有一个面试必考点哪些集合是线程安全的ArrayList线程不安全Vector线程安全已过时HashMap线程不安全Hashtable线程安全已过时ConcurrentHashMap线程安全推荐。Vector和Hashtable虽然线程安全但实现方式是在方法上直接加synchronized多线程竞争时并发度极低性能很差。现代Java开发里基本不用它们替代方案分别是CopyOnWriteArrayList和ConcurrentHashMap。ConcurrentHashMap在JDK 1.8之后的实现是CASsynchronized锁头节点锁粒度比Hashtable锁整个对象细得多所以并发性能更好。CopyOnWriteArrayList则是读时不加锁、写时复制整个数组适合读多写少的场景比如监听器列表。fail-fast机制也值得单独记一下ArrayList和HashMap的迭代器是fail-fast的意思是迭代过程中如果结构被修改增删元素会抛出ConcurrentModificationException。但这个机制不保证一定发生它只是用modCount属性做快速检测属于并发修改的保护手段不是强一致性保证。实际项目里如果在遍历集合时需要删除元素不要用集合的remove()方法而应该用迭代器的remove()方法这样不会触发fail-fast。这个点我代码评审时经常纠正别人。5. 异常处理写好异常项目少出问题5.1 异常体系结构受检与非受检的边界在哪Java的异常体系顶层是Throwable下面分Error和Exception两个分支。Error代表JVM层面的严重错误比如StackOverflowError、OutOfMemoryError程序一般不该去捕获它们也基本捕获不了。Exception下面又分为受检异常Checked Exception和非受检异常RuntimeException。受检异常是编译期强制要求处理的异常比如IOException、SQLException不处理就编译不过。非受检异常是运行时异常比如NullPointerException、IllegalArgumentException、IndexOutOfBoundsException编译期不强制处理。这是Java的一个设计特点很多现代语言如C#、Kotlin并不区分受检异常。面试时如果面试官问“你对受检异常怎么看”可以表达自己的观点受检异常强制调用方处理但有时会导致代码里充斥着try-catch而忽略了真正的错误处理逻辑。我自己的经验是Service层抛出受检异常会导致Controller层被迫处理代码很脏。所以很多实际项目的做法是把业务异常定义成RuntimeException的子类配合RestControllerAdvice做统一异常处理。比如定义BizException extends RuntimeException抛的时候带错误码和错误消息全局异常处理器统一捕获并返回给前端。这样代码干净、调用方无感、错误信息还能统一管理。5.2 善用try-with-resources与正确的异常处理姿势JDK 7引入了try-with-resources语法让资源管理变得优雅了很多。以前关闭流是这样写的BufferedReader reader null; try { reader new BufferedReader(new FileReader(test.txt)); // 读文件... } catch (IOException e) { log.error(读取文件失败, e); } finally { if (reader ! null) { try { reader.close(); } catch (IOException e) { log.error(关闭流失败, e); } } }用try-with-resources之后try (BufferedReader reader new BufferedReader(new FileReader(test.txt))) { // 读文件... } catch (IOException e) { log.error(读取文件失败, e); }写法简化了很多而且实现了AutoCloseable的资源会自动关闭关闭顺序和资源声明顺序相反。这里有个容易忽略的细节try-with-resources语法在编译后生成的字节码会在try块中发生异常时把close()抛出的异常作为suppressed异常附加到主异常上而不是覆盖主异常。用getSuppressed()方法可以拿到这些被抑制的异常。面试能提到这个说明你真的读过相关源码。关于异常处理的另一个常见问题是catch了异常但是不做任何处理只打个空日志或者干脆不写。这是我最讨厌的代码之一。正确的做法有三个层次向上抛出让上层处理、记录完整日志堆栈信息后转成业务异常抛出、catch后做降级处理并记录日志。原则是要么处理要么抛出绝不能静默吞掉。因为这个坑我排查线上问题时花费的冤枉时间太多了一条日志里看到catch块里只写了e.printStackTrace()日志文件里还只是无用的堆栈完全无法定位问题所在。5.3 日志与异常把堆栈信息记录完整说到日志我这些年踩过的坑就是很多人catch异常后日志只记录异常消息比如log.error(xxx失败 e.getMessage())这在多线程、高并发的环境里根本没有办法定位到具体是哪一行代码出的问题。正确做法是用log.error(操作xxx失败参数xxx, e)把异常对象作为最后一个参数传进去这样日志里会输出完整的堆栈信息。第二个参数的意义在于异常波基和上下文参数一起输出排查问题时能还原现场。还有一个容易被忽视的点不要吞掉检查型异常的读取操作。我在数据库操作、文件操作相关的代码里见过太多免检型的catch结果异常被吞了后续代码继续运行整个业务数据都错了。不管是什么异常如果一个catch块决定不抛出、不处理那么至少要记录日志并返回一个合理的fallback值而且要打warning甚至error级别让监控系统能感知到。6. 复习自查用这些题检验你的基础牢不牢整理完上面的知识点我出一套自查题大家复习完之后可以对着检验一下自己的掌握程度Integer a 127, b 127; a b结果是如果换成128呢为什么float f 1.1;这行代码能编译通过吗为什么重写和重载的本质区别是什么Override注解的作用是什么抽象类和接口在Java 8之后还有什么本质区别实际项目中怎么选String s1 new String(abc); String s2 abc; s1 s2结果是s1.equals(s2)结果是字符串拼接在循环体内做和循环体内用StringBuilder性能差距有多大为什么HashMap的put流程是什么链表转红黑树的条件和原因是什么ArrayList在遍历时能不能删除元素安全的方式是什么受检异常和非受检异常的区别是什么业务异常应该继承哪一类try-with-resources为什么比传统finally写法更好suppressed异常是什么这十道题如果都能答清楚Java基础的上半部分基本是过关的。答不清楚的地方就回去把对应的章节再翻一遍。我自己复习时有个习惯不是背书而是每看一个知识点就尝试用自己的话讲给旁边的人听讲不出来就是没掌握。这个方法比闷头刷题有效得多。这篇“上”篇覆盖了数据类型、面向对象、字符串、集合和异常处理写完之后我自己也重新理清了几个以前模糊的概念特别是异常处理那块和HashMap红黑树转化的细节。后续如果大家需要我可以继续整理“下”篇把JVM内存模型、类加载机制、多线程并发、泛型与反射这些进阶内容补齐。复习Java基础这件事看起来是在应付面试但实际上把这些底层逻辑弄明白了写业务代码的时候你会发现自己对很多问题都有了一种“原来如此”的掌控感。
返回列表