ARTICLE DETAIL

资讯详情

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

Java零基础入门:从JDK环境搭建到并发编程一条龙

Java零基础入门:从JDK环境搭建到并发编程一条龙 很多初学者第一次建 Java 工程起的名字往往就叫 java--1。我当年也一样从命名到环境折腾了整整一个晚上。这个“第一工程”看起来简单背后却要跨过 JDK 安装、环境变量、第一段编译命令、面向对象这些坎每一道都能劝退一批人。这篇文章不按教科书顺序讲而是按我实际带新手做项目时的顺序来先把 Java 环境从零跑通再手写第一个程序然后把面试和日常开发里最高频的知识点拆开揉碎最后附上一张避坑速查表。适合零基础、正在准备 Java 基础面试、以及虽然会写代码但想补一轮基础的同学。也可以把它当成一条 Java 学习路线的浓缩版从安装到并发一条线走到底。1. 第一道门槛JDK 安装与 Java 运行原理1.1 先分清 JDK、JRE、JVM 再下载许多人卡在第一句我该下载 JDK 还是 JRE现在 JDK 版本里已经完整包含了 JRE 和 JVM所以日常开发只要认准 JDK。三者的关系我习惯跟新手类比成厨房JDK 是连锅带厨具的整套厨房JRE 是能加热半成品的小厨电JVM 则是那个真正把食材做成菜的灶台。JDK 里包含 javac 编译器、java 运行命令、jar 打包工具JRE 只负责运行而 JVM 是 Java 虚拟机的缩写。Java 能“一次编译、到处运行”靠的正是每个系统上装一个对应版本的 JVM编译出的 .class 文件到哪里都由对应的 JVM 解释执行。下载之前先定版本。现在建议直接上 17 或 21 这类 LTS 长期支持版本。Java 8 虽然老项目里还有很多但新面试题基本都在问 17 以上才有的特性Spring Boot 3 也明确要求 Java 17 起步。下载渠道能用官网就用官网OpenJDK 发行版里推荐 TemurinAdoptium 出品和 Oracle JDK学习阶段两者没区别公司项目则看团队约定。安装过程一路 Next 就行但装完之后先别急着开 IDE把命令行那关过了再上 IDE后面你会感谢自己这个决定。注意别在 JDK 8 和最新版之间来回折腾一个阶段只用一个 JDK。面试被问到“你用的哪个版本”能说出 LTS 版本的含义和为什么不用最新版就是加分点。1.2 环境变量配置到底配的是什么很多教程让新手配 JAVA_HOME、Path、Classpath 三样Classpath 现在完全可以不配JDK 5 之后编译器能自动找到核心类库真正决定你能不能在任何目录下使用 java 命令的是前两个。JAVA_HOME 就是一个变量名指向 JDK 安装根目录比如 D:\Java\jdk-17。Path 里追加 %JAVA_HOME%\bin系统才能在任何目录下找到 java.exe 和 javac.exe。用打车软件类比你在命令行下单系统得知道司机在哪才能派单你在命令行输入 java系统也得知道 java.exe 放在哪里才能调起来。如果不配 Path每次都要打完整路径自然没法干活。Windows 配置步骤大概是这样的右键“此电脑” - “属性” - “高级系统设置” - “环境变量”。新建系统变量 JAVA_HOME值填 JDK 安装目录。编辑 Path新增一行 %JAVA_HOME%\bin。重新打开 cmd输入 java -version。Linux 和 macOS 一般是改 ~/.bashrc 或者 ~/.zshrc用 export 导出 JAVA_HOME 和 PATH 两行然后 source 一下macOS 也可以用 /usr/libexec/java_home 自动定位。这里不展开因为大多数初学者从 Windows 起步把 Windows 这条路走通就够跑了。1.3 验证环境是否装好java 和 javac 有什么区别在命令行输入 java -version 能看到 JRE 信息输入 javac -version 能看到编译器信息。经常有人只验证了 java 没有验证 javac结果编译第一个程序时遇到“javac 不是内部或外部命令”。这多半是环境变量只配了一半或者命令行窗口没有重新打开。新开的窗口才会重新读取环境变量这一条特别容易忽略。这一阶段常见问题还有旧 JDK 卸载不干净导致环境变量里残留老版本路径明明控制面板里删了java -version 还是弹出旧版本。Windows 上优先检查 C:\Program Files\Java、C:\Program Files (x86)\Java 以及环境变量里的历史路径发现就清理实在不行用卸载工具把注册表残留一起清干净。这类问题排查本身不算核心知识但能帮你省下一个星期的折腾时间。2. 第一个 Java 程序从 HelloWorld 到工程结构2.1 手写一次编译运行比 IDE 一键运行更值得用记事本或 VS Code 新建一个 Hello.java输入几行代码然后打开命令行手动编译运行public class Hello { public static void main(String[] args) { System.out.println(Hello, java--1); } }javac -encoding UTF-8 Hello.java java Hello第一行 javac 负责把源码编译成字节码文件 Hello.class第二行 java 负责让 JVM 加载并运行这个 class 文件。初学者最容易犯的错是运行的时候写成 java Hello.class。去掉扩展名不是故意刁难因为 JVM 按类名找类不按文件名找文件传 Hello.class 反而会报“找不到或无法加载主类”。还有文件编码问题Windows 版 javac 默认按系统编码读源码源码里有中文就容易乱码所以编译命令里显式加 -encoding UTF-8。这两个点就是劝退新手最多的两个坑提前写在这里省得你再踩一遍。2.2 为什么要有 package先学会给代码“装进抽屉”单文件跑通以后就该理解 package 了。package 相当于文件系统中的文件夹也是 Java 类命名空间的一部分。真实工程里不会允许所有类平铺在同一个根目录否则几百个类互相重名会乱到不可收拾。常见包名用反写的公司域名比如 com.example.study每个包对应磁盘上的一个目录这个结构叫包结构。类名加上包名在一起就是全限定名比如 com.example.study.HelloMain。package 还有两个实际作用影响访问权限控制并避免类名冲突。一个 JVM 里可以同时存在两个同名但包名不同的类这在兼容旧接口或者集成多版本库时很有用。我见过不少新人问“IDEA 里明明建了类但编译时总说找不到”十有八九是包名和目录不一致或者 import 写错。记住一条经验在 IDE 里移动类到默认包这个操作往往是后面各种离奇报错的开始。2.3 从命令行走入 IDEIntelliJ IDEA 里建第一个工程有了命令行基础再用 IDE 就是事半功倍。推荐 IntelliJ IDEA Community 版免费而且 Java 生态用得最广。新建项目的关键点是正确指定 JDK打开 Project Structure能看到 SDK 设置选中你刚装好的 17 或 21如果 IDEA 里类名标红优先检查 SDK 有没有选对。然后建一个包再建一个类把上面的 Hello 代码复制进去右键 Run 就可以跑起来。为什么坚持先命令行再 IDE因为 IDE 把 javac、java 两条命令和 classpath 都藏起来了一旦报错你不知道是哪一步出问题。有过一次手工编译的经验再回头看 IDE 的报错面板心里会非常踏实。这个阶段也可以建一个最简单的 Maven 项目感受标准目录结构src/main/java 放源码src/main/resources 放配置和静态文件。但别急着研究 Maven 依赖管理那是一座新的山等你能独立写完一个小项目再爬不迟。3. 语法地基数据类型、流程控制与高频 String/集合容器3.1 八种基本类型、包装类与 Integer 缓存Java 基础面试第一题大概率是数据类型。基本类型有八种byte、short、int、long、float、double、boolean、char。除了占用内存不同关键是取值范围。写代码时 long 类型字面量要加 Lfloat 要加 F细节上经常有人漏。基本类型没有方法所以 Java 给每个基本类型配了包装类Integer、Long、Boolean 等主要用于集合框架。自动装箱和拆箱是编译器自动完成的但有一个经典大坑Integer a 100; Integer b 100; System.out.println(a b); // true Integer c 200; Integer d 200; System.out.println(c d); // false原因不复杂Integer 缓存了 -128 到 127 之间的对象在这个范围内直接返回缓存对象超出范围就 new 新对象。所以包装类型比较值必须用 equals()不能用 。这道题几乎是八股文必考但很多人只背结论不理解缓存范围为什么是 -128 到 127面试官追问“为什么是这个区间”就答不上来。本质是内存与性能的折中取值范围小的整数太常用了能复用就复用。3.2 流程控制里的坑switch 空值、for 删除、循环效率流程控制本身不难if/else、for、while 大家都会写。真正容易出问题的是那些反直觉的细节。switch 在 Java 7 之后支持字符串但匹配一个 null 引用时会直接抛 NullPointerException因为 switch 底层会调用 hashCode()Java 14 引入箭头语法后switch 可以作为表达式使用但依然不会匹配 null。如果你确实要处理空值可以先判空再 switch或者干脆用 if 链。这个“switch 空数据”的问题在接外部接口时特别容易出现比如解析返回的可空字符串。第二个坑是增强 forfor-each里删除元素。for-each 内部使用迭代器如果循环里直接调用 list.remove()下一次循环时就会抛 ConcurrentModificationException。正确做法是使用 Iterator.remove()或者先把要删除的元素收集起来循环结束后统一 removeAll()。还有一个性能问题循环里用 str i 拼接字符串每次都会 new 一个 String复杂度是 O(n²)改用 StringBuilder 的 append() 能降到 O(n)。StringBuffer 做的是同样的事但方法加了 synchronized线程安全但性能略差单线程场景下都用 StringBuilder。3.3 数组排序的小考点冒泡排序怎么写出区分度面试也常让手写冒泡排序。题目不难但能拉开差距的是优化外层循环跑完 n-1 轮如果某一轮里没有发生任何交换说明数组已经有序可以直接 break。这个细节很多人写不出来写出来的就能体现你考虑过最好情况的时间复杂度。int[] arr {5, 1, 4, 2, 8}; for (int i 0; i arr.length - 1; i) { boolean swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) break; }真正的考点不是交换逻辑而是“内层循环为什么要减 i”以及“swapped 标记怎么提现你对数据状态的理解”。把这个例子背熟比刷十道毫无关联的题有用。3.4 String 的不可变性与对象深度拷贝String 是不可变类每个看似修改的操作其实都返回新字符串。这个设计让字符串可以安全缓存和复用也天然适合当 Map 的 key。字符串常量池是 JVM 里专门放字面量字符串的区域两个内容相同的字面量通常指向同一个对象但用 new String(a) 会在堆里再创建一个新对象。这也是 与 equals 区别的经典案例。顺着 String 说开去面试还常问“对象深度拷贝”。浅拷贝只复制字段如果字段是引用类型新旧对象会共享同一个内部对象深拷贝要把整个对象图都复制出来。实现方式有两种主流一是重写 clone() 后逐层手动拷贝代码繁琐且容易漏二是用 JSON 序列化反序列化把对象转成 JSON 字符串再转回来对嵌套结构很省心。我在实际项目里用得更多的是第二种只要别放在超高频率的调用链路里效果很稳。4. 面向对象落地封装、继承、多态与动态代理4.1 封装private 真的只是“不让人访问”吗很多新手觉得封装就是字段写成 private再生成 getter/setter纯属走个流程。我的观点是封装的本质是定义接口边界、保护内部约束。比如账户类里有 balance 字段直接公开的话外部随时可以把它改成负数但提供一个 withdraw() 方法你就可以在方法里校验余额、记录日志、控制并发。getter/setter 也不是必须的只有字段确实需要暴露才生成。实际项目中我推荐尽量少写 setter尤其在并发环境里可变字段越多数据被意外改坏的风险越高。访问修饰符还有一层包级关系public 全局可见protected 包内和子类可见默认不加修饰符包内可见private 仅类内可见。面试常问 protected 和默认访问权限的区别很多背过答案的人一写代码就错就是因为没在真实包结构里试过。建议这个阶段做一个 demo把四种修饰符各建几个类分别放到不同包里实际观察哪里标红比死记表格有用得多。4.2 继承与多态父类引用到底调用谁的实现继承是面向对象里最容易被过度使用的关系。父类 Animal子类 Dog 和 Cat都重写 sound() 方法然后用 Animal 类型变量接收 Dog 对象调用 sound() 时执行的是 Dog 的实现。这就是动态绑定。也正因为它好用才会出现“为了复用几个方法就强行继承”的坏味道。判断能不能用继承看子类和父类是否满足 is-a 关系子类是不是一个更具体的父类。如果不满足就考虑组合has-a。比如“车 has-a 引擎”就不应该让 Car 继承 Engine。接口和抽象类的选择也是高频考点。抽象类本质是类可以写字段和通用方法适合模板方法场景接口是能力约定Java 8 之后可以有 default 方法适合定义行为契约。现在的框架设计都强调面向接口编程依赖注入也优先注入接口这样替换实现类时不用改调用方代码。单独理解不难但配合动态代理一起看就能明白 Spring AOP 背后那套逻辑。4.3 动态代理与 InvocationHandler从面试题到框架原理动态代理是 Java 面试绕不开的一道题。先理解概念运行时动态生成代理类而不是像静态代理那样提前写好一个类。JDK 动态代理要求目标类必须实现接口核心是 java.lang.reflect.Proxy 和 InvocationHandler。写一个最小例子interface HelloService { void sayHello(); } class HelloServiceImpl implements HelloService { Override public void sayHello() { System.out.println(hello java); } }然后通过 Proxy.newProxyInstance 创建代理对象传入类加载器、接口数组和一个 InvocationHandler在 invoke() 方法里可以在目标方法前后加逻辑HelloService proxy (HelloService) Proxy.newProxyInstance( HelloService.class.getClassLoader(), new Class[]{HelloService.class}, (proxyObj, method, methodArgs) - { System.out.println(before); Object result method.invoke(new HelloServiceImpl(), methodArgs); System.out.println(after); return result; } ); proxy.sayHello();这段代码看着吓人拆开就是三件事代理对象挂在哪个类加载器下、实现哪些接口、每个方法调用时走哪个处理器。Spring AOP 在目标对象是接口时就基于这套机制MyBatis 的 Mapper 也是通过动态代理把接口方法映射成 SQL。理解动态代理之后很多框架的“黑魔法”就变成了透明的机制。4.4 聚合、组合与对象图代码里的设计感“java聚合”这个词看着像新概念其实是 UML 里描述整体与部分关系的方式。聚合是弱“拥有”部门和员工部门没了员工还在组合是强“拥有”人和心脏人没了心脏也就没了。代码层面的区别体现在构造时机和生命周期上。比如订单包含多个订单项通常用组合理解因为订单项脱离订单没有业务意义班级和学生则更像聚合学生不在这个班了仍然是独立个体。设计的时候不要死记概念而是问自己这个部分对象能不能脱离整体存在如果不能就让整体负责创建并销毁它如果能就通过构造函数把外部对象传进来。这组概念不仅能答面试还能帮你在写 service 层时想清楚对象间的依赖方向减少循环引用带来的死循环和难排查的 bug。5. 真实需求练手用 POI 操作 Word 并生成图表5.1 需求场景报表导出为什么绕不开 POI实战里最常见的 Java 业务需求之一就是把数据库查询结果按固定模板导出成 Word 或 Excel。Apache POI 在 Java 生态里基本是事实标准Excel 操作对应 HSSFWorkbook/XSSFWorkbookWord 对应 XWPFDocumentdocx和 HWPFDocument老式 doc。现在新项目几乎不碰 doc直接学 XWPFDocument 就够了。一篇分享如果只讲读文本、写文本就太浅了真实报表往往还要求表格、图表、封面、页眉页脚。我用一次实际需求举例业务方要一份“月度销售分析报告”Word 里既有表格数据又要一张柱状图。如果只把数据塞进文本段落代码很简单一旦加入图表方案就要重新选型。5.2 POI 原生能生成 Word 图表吗这个问题很多人问过先给结论POI 的 Word 模块对图表支持很弱。Excel 模块有成熟的 XSSFChartPowerPoint 模块也有 XSLFChart但 Word 模块的图表 API 不完善。想用 POI 原生操作 Word 里的图表得手工往 drawingML 的 XML 里塞数据开发成本高维护也难受。我的建议是换一种方式用 JFreeChart 生成一张 PNG 图片再用 XWPFDocument 插入到 Word 里。图片形式虽然不能像原生图表那样二次编辑但大部分业务报表打印和查阅完全够用。依赖大致如下dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency dependency groupIdorg.jfree/groupId artifactIdjfreechart/artifactId version1.5.4/version /dependency核心流程三步用 JFreeChart 的 ChartFactory 创建数据集和图表对象调用 ChartUtils.saveChartAsPNG 把图片写到字节数组再用 XWPFDocument 的 createParagraph 和 run.addPicture 插入图片。JFreeChart chart ChartFactory.createBarChart(月度销量, 月份, 销量, dataset); byte[] imageBytes ...; // 保存 PNG 到字节数组 XWPFParagraph p document.createParagraph(); XWPFRun run p.createRun(); run.addPicture(new ByteArrayInputStream(imageBytes), XWPFDocument.PICTURE_TYPE_PNG, chart.png, Units.toEMU(400), Units.toEMU(250));代码本身不难难点都在细节里。图片名字参数虽然只是内部标识但别传空字符串某些 POI 版本会有问题插图大小要用 Units.toEMU 把像素转换成文档里的 EMU 单位否则图片会突然变小或者跑偏这是我第一次做的时候踩过的坑。5.3 大表格导出内存、模板与流式处理用 POI 导出大文档时最容易出问题的就是一次性构建几百 MB 的文档把内存撑爆。XSSFWorkbook 会把整张表放内存所以 POI 提供了 SXSSFWorkbook 做流式写 Excel但 SXSSF 是 Excel 专有的Word 这边没有完整的流式对应方案。实际项目里我一般用三个招数一控制单文档规模按章节分多个 Word 文件二用模板替换把 docx 模板放进 resources读取后替换固定占位符适合格式固定的报告三数据库查询分批进行边查边写避免一次性加载全量数据。导出下载还有一个容易被忽略的细节文件名里的中文要做 URL 编码否则浏览器保存时文件名会截断或者乱码。我习惯在 Content-Disposition 里同时保留 filename 和 filename* 两个字段兼容老浏览器。真实用户报障里十个导出问题三个在文件打不开三个在文件名乱码真不是笑话。6. 面试高频专项并发与数据一致性怎么答6.1 先搞懂线程安全的三个层面可见性、原子性、有序性Java 并发不能上来就背 synchronized 和 ReentrantLock要先理解为什么需要并发安全。原因集中在三个层面可见性一个线程改了变量另一个线程不一定立刻看到、原子性复合操作可能被中途打断、有序性编译器和 CPU 可能重排序。volatile 能保证可见性和一定程度的有序性但不保证原子性。很多人问 volatile 和 synchronized 的区别简单答就是volatile 像给变量贴了一张“最新状态公告”synchronized 则把整段操作关进小黑屋解决的问题层次不同。原子性可以用 AtomicInteger 这类 CAS 工具实现。CAS 是乐观锁思路不加锁但通过比较内存值来判断是否修改成功失败就自旋重试。面试能答到“CAS 自旋”基本过关但你要能说出 ABA 问题值从 A 变成 B 又变回 ACAS 无从感知需要用 AtomicStampedReference 加版本号。能聊到这一层面试官一般就看得出你真正读过并发包源码。6.2 AQS并发框架的地基“aqs java”已经成了面试高频搜索词说明它确实是必刷点。AQS 全称 AbstractQueuedSynchronizer翻译是抽象队列同步器。ReentrantLock、Semaphore、CountDownLatch 都是基于它实现的。一句话理解它维护了一个 volatile 的 state 变量表示资源状态加一个 FIFO 的等待队列线程获取资源失败就进队列排队释放资源后唤醒队首。面试不用背完整源码但要有能力画出“锁申请失败入队、成功后出队”的流程。ReentrantLock 的可重入体现在一个线程持有锁后再次加锁state 累加释放时递减到 0 才算真正释放。能从 state 的加减推导出可重入比背一百句“synchronized 是可重入的”有说服力。还要知道公平锁和非公平锁的差异非公平锁会先尝试直接抢一次 state抢不到才排队所以吞吐量高但极端情况下可能饿死排队线程公平锁按 FIFO 严格排队性能略低但更稳定。6.3 数据一致性从单机事务到分布式最终一致“java怎么保证数据一致性”是搜索量很高的词但问法不同答案侧重点也不同。单机范围内核心是数据库事务的 ACID 和隔离级别。比如转账扣钱和加钱必须在一个事务里要么都成功要么都失败。Java 里用 Transactional 只是声明式事务底层还是靠数据库提交和回滚。别忽略传播行为REQUIRED 用得最多它代表“加入已有事务”如果内部方法 try-catch 吞掉异常事务仍然可能全体回滚这是一个经典大坑。分布式场景下常见的是最终一致性而不是强一致。实现方式有几种本地消息表把业务操作和消息写入放在同一个本地事务里再异步投递消息事务本质也是两阶段确认还有基于状态机的对账补偿。我自己最常用的是本地消息表加定时补偿扫描原因很简单它不依赖特殊中间件用数据库本身的保证就能实现逻辑直白出问题容易排查。面试能说清“为什么多数业务允许最终一致”比堆一堆名词更有价值。7. 避坑速查表从环境到运行我替你踩过的坑很多经验写在文档里没人看遇到问题时又找不到。我把带新人时反复遇到的问题集中成一张表方便对号入座。报错或现象最常见原因解决办法javac 不是内部或外部命令Path 没配或 cmd 没重开检查 JAVA_HOME 和 Path重开命令行中文乱码源码编码和 javac 编码不一致保存为 UTF-8编译加 -encoding UTF-8找不到或无法加载主类运行时带了 .class 扩展名用 java 类名不要带 .class警告: 源发行版 17 需要目标发行版 17Maven/IDE 编译级别没对齐设置 Java compiler target 为 17重新加载项目IDEA 类名标红Project SDK 未选择Project Structure 里指定 JDK 17/21卸载 JDK 提示程序包有问题旧版本注册表残留控制面板卸载后用清理工具扫描残留java -version 仍是旧版本多个 JDK 并存且旧路径排在前清理 Path 历史路径必要时清注册表残留ConcurrentModificationException增强 for 循环里直接 remove用 Iterator.remove 或收集后统一删除Integer 用 比较不正确超出 -128~127 缓存范围包装类一律用 equalsPOI 插入图片位置不对或大小夸张没用 Units.toEMU用 Units.toEMU 转换像素使用标准 PICTURE_TYPE 常量这张表还可以继续扩展但核心思路是报错信息是线索不要看到报错就盲目复制网上的命令。先检查环境变量、编码、类名、JDK 版本这四个变量对不对再谈其他。我带人时最常说的是先学会读异常栈从第一行往下看到 Caused by这才是真正的排查能力。最后分享一点个人体会。java--1 这个名字在很多工程里就是一个学习项目但它值得你认真对待。环境配好、第一个程序跑通、面向对象和集合框架过一遍之后你已经把 Java 自学路线上最陡的坡爬完了。往后学 Spring、MySQL、Redis 的时候你还会不断回到这些基础上来。面试时被问“做过什么项目”与其背一个包装出来的商城系统不如把一个 hello 工程从环境到运行再到底层原理讲透这比任何八股文都更有说服力。
返回列表