ARTICLE DETAIL

资讯详情

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

Java核心基础深度解析:从面向对象到并发编程的稳定态知识体系

Java核心基础深度解析:从面向对象到并发编程的稳定态知识体系 1. 为什么2021年的Java基础总结今天依然值得看每次看到“Java基础知识点总结”这样的标题很多朋友可能会想这都什么年代了Java都迭代到21了还看2021年的总结是不是过时了作为一个从Java 5时代一路写过来的老码农我得说恰恰相反。2021年这个时间点正好是Java 8作为绝对主流、Java 11开始普及、而Java 17尚未大规模上线的“黄金窗口期”。这个时期的Java基础沉淀了从Java 5到Java 11这十几年间最核心、最稳定、也最被企业级开发验证过的语言特性和编程思想。它不像Java 8之前那样“古朴”也不像Java 17之后引入了太多需要特定场景才能理解的预览特性。这份总结更像是一份“经典Java核心语法与API的稳定态地图”。对于初学者它能帮你避开那些过于前沿但实际应用还不广的语法糖直击面试和工作中最高频的考点。对于有经验的开发者它是一次系统性的“回炉重造”帮你理清那些习以为常但可能理解并不透彻的概念比如泛型的类型擦除到底擦除了什么、String的不可变性在内存中是如何体现的。今天我们就抛开那些华而不实的框架和中间件回到代码本身把Java这门语言的基石彻底摸透。这份总结的目标不是罗列API而是带你理解每一个知识点背后的设计意图和运行机制让你写出更健壮、更高效、也更容易被同事理解的代码。2. 面向对象从“会用”到“懂为什么这么用”面向对象是Java的魂但很多人学了几年可能还停留在“知道有封装、继承、多态”的层面。我们得往深了挖。2.1 类与对象不止是new一下创建一个对象new关键字背后发生了什么这直接关系到JVM的内存管理和初始化顺序。类加载当JVM首次遇到new MyClass()时如果MyClass尚未被加载则会触发类加载过程加载、验证、准备、解析、初始化。在“准备”阶段会为类变量static变量分配内存并设置默认初始值0, false, null等。内存分配在堆Heap中为新生对象分配内存空间。内存大小在类加载完成后就可确定。初始化零值将对象内存空间中的所有实例变量设置为对应数据类型的零值。这一步确保了即使构造器里没赋值字段也有默认值。设置对象头JVM会在对象头中存储一些元数据如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。这是实现同步、GC和对象标识的基础。执行构造器这才是我们写的构造器被调用的时刻。它会按照代码逻辑对实例变量进行显式初始化。这里有个关键顺序先执行父类构造器隐式或显式调用super()再按声明顺序初始化实例变量包括实例变量赋值和实例代码块最后执行构造器体内的代码。注意很多人混淆了“默认初始化”和“显式初始化”。在构造器执行前实例变量已经被JVM赋予了零值默认初始化。构造器里的赋值是“显式初始化”。理解这个顺序对排查一些诡异的NullPointerException很有帮助比如在构造器里调用了一个可被重写的方法而该方法依赖子类尚未初始化的字段。2.2 继承的深水区构造器链与初始化顺序继承不只是“复用代码”它建立了一种严格的初始化顺序约束。看这段代码class Parent { private String parentField initParentField(); static { System.out.println(Parent static block); } { System.out.println(Parent instance block); } Parent() { System.out.println(Parent constructor); } private String initParentField() { System.out.println(Parent field init); return parent; } } class Child extends Parent { private String childField initChildField(); static { System.out.println(Child static block); } { System.out.println(Child instance block); } Child() { System.out.println(Child constructor); } private String initChildField() { System.out.println(Child field init); return child; } }执行new Child()的输出顺序是Parent static block Child static block Parent field init Parent instance block Parent constructor Child field init Child instance block Child constructor这个顺序是铁律父类静态 - 子类静态 - 父类实例变量/代码块 - 父类构造器 - 子类实例变量/代码块 - 子类构造器。掌握它你就能理解为什么在父类构造器中调用可重写方法是危险的此时子类字段还是零值。2.3 多态与动态绑定方法调用的真相“编译看左边运行看右边”是口诀但背后的机制是动态绑定Dynamic Binding。对于实例方法非static、非private、非finalJVM在运行时根据对象的实际类型来决定调用哪个方法。这个信息存储在方法区元空间的类信息中具体是通过虚方法表Virtual Method Table来实现的。每个类都有一个vtable里面记录了该类所有虚方法的实际入口地址。当调用obj.method()时JVM会获取obj实际类型的vtable找到对应方法的地址进行调用。而private、static、final方法以及构造器由于不具备多态性采用的是静态绑定在编译期就确定了调用的目标。2.4 抽象类与接口的演进与选择2021年Java的接口已经非常强大了。在Java 8之前接口和抽象类的选择很清晰接口定义行为契约抽象类提供部分实现。Java 8为接口引入了默认方法default method和静态方法这使得接口也能提供方法实现。那么现在该如何选抽象类状态可以拥有实例变量字段。构造器可以有构造器虽然不能直接实例化用于初始化抽象类定义的状态。访问控制方法可以是public,protected,private。使用场景当你需要定义一些共享的状态或行为并且这些状态/行为在子类间有共同的初始化逻辑时。例如模板方法模式中抽象类定义算法骨架。接口状态只能有静态常量public static final。构造器没有构造器。方法Java 8后可以有抽象方法、默认方法、静态方法。Java 9还可以有私有方法。使用场景定义能力或角色。一个类可以实现多个接口从而实现多重“类型”。这是定义行为契约、支持策略模式、函数式编程通过FunctionalInterface的首选。简单决策树如果需要定义状态或强制性的公共构造逻辑用抽象类。如果只是定义行为或类型优先使用接口。现代Java开发中接口的使用频率远高于抽象类。3. 核心API与集合框架高效使用的底层逻辑Java的API设计哲学是“提供足够的抽象同时暴露必要的控制”。理解其底层才能避免性能陷阱。3.1 String为什么说“几乎”不可变String的不可变性是Java安全的基石之一。所谓不可变是指一旦一个String对象被创建其内部的字符数组final char[] valueJava 9后是byte[]引用和内容就不可更改。所有看似修改的操作如concat,substring在旧版本中replace实际上都是创建了一个全新的String对象。带来的好处安全性作为参数传递时不用担心被意外修改。广泛用于网络连接、文件路径、类加载等关键场景。线程安全不可变对象天生线程安全。缓存哈希值String的hashCode()会缓存第一次计算的结果因为值永不变这大大提升了作为HashMap键的性能。字符串常量池这是实现不可变性的最大收益。JVM有一块特殊内存区域方法区的一部分用于存储字面量字符串和显式intern()的字符串。当创建字面量字符串时JVM会先检查池中是否存在存在则返回引用不存在则创建并放入池中。这节省了大量内存并使得字符串比较可以用但仅限于字面量和intern()后的不推荐在常规代码中用比较字符串内容。注意点String的不可变是针对对象本身但String类型的引用是可变的。String s a; s b;这里s引用指向了新的对象而非修改了旧对象。3.2 集合框架选型不止是ArrayList和HashMap集合框架的选用核心是权衡访问、插入、删除的性能以及内存开销和线程安全性。集合类型底层实现特点与适用场景注意事项ArrayList动态数组随机访问快O(1)尾部插入快。但中间插入/删除慢需移动元素。默认初始容量10扩容1.5倍。预估数据量构造时指定初始容量new ArrayList(1000)避免多次扩容拷贝。非线程安全。LinkedList双向链表头部/中间插入、删除快O(1)但随机访问慢O(n)。实现了Deque接口可作队列/栈。内存开销大每个元素需存储前后节点引用。绝大多数场景ArrayList更优除非有大量中间位置的增删。HashMap数组链表/红黑树基于哈希表的键值对。Java 8后链表长度8且数组容量64时链表转为红黑树提升最坏情况性能。默认负载因子0.75。键对象必须正确重写hashCode()和equals()。高并发下需用ConcurrentHashMap。允许null键和值。LinkedHashMap哈希表双向链表在HashMap基础上维护了插入顺序或访问顺序的链表。可用来实现LRU缓存通过重写removeEldestEntry。比HashMap稍慢内存开销略大。TreeMap红黑树基于红黑树的有序键值对按键的自然顺序或Comparator排序。增删查改时间复杂度O(log n)。键对象必须实现Comparable接口或在构造时提供Comparator。HashSet基于HashMap内部封装了一个HashMap值存储在一个固定的Object对象上。本质是HashMap的键集所有HashMap的注意事项都适用。ConcurrentHashMap分段锁/ CAS高并发下的线程安全HashMap。Java 7采用分段锁Java 8改为synchronized锁链表头/树根CAS。性能远优于Hashtable。迭代器是弱一致性的不保证能反映创建后的所有修改。size()方法返回值是近似值。关键原理补充HashMap扩容当元素数量超过容量 * 负载因子时触发。创建一个新数组2倍原容量然后遍历旧数组所有元素重新计算哈希值分配到新数组的新位置。这是一个相对耗时的操作。hashCode()与equals()的契约这是集合框架正确工作的基石。必须保证1) 两个对象equals为true则它们的hashCode必须相等2) 反之hashCode相等equals不一定为true哈希冲突。违反此契约会导致HashMap、HashSet等无法正确工作。3.3 泛型与类型擦除编译器的“魔法”泛型是Java 5引入的重大特性但它采用的是“类型擦除”来实现的这是为了兼容老版本字节码。理解擦除才能理解泛型的局限。什么是类型擦除在编译后所有的泛型类型信息都会被移除擦除。例如ListString和ListInteger在运行时都是List原始类型。类型参数会被替换为它们的边界未指定边界则替换为Object。带来的影响与应对instanceof 检查你不能写list instanceof ListString因为运行时没有String这个类型信息。只能检查list instanceof List。不能创建泛型数组new T[]是不允许的因为运行时不知道T的具体类型。通常用new Object[]然后强制转换或者使用ArrayList等集合。泛型类中的静态成员静态变量和方法属于类而非实例。因此MyClassT中的静态成员不能使用类型参数T。所有实例共享同一个静态成员。获取泛型实际类型虽然被擦除了但可以通过反射获取父类或接口的泛型参数化类型ParameterizedType这就是Spring等框架实现依赖注入时获取RepositoryUser中User类型的方法。4. 异常处理不仅仅是try-catch异常处理是构建健壮程序的关键但用好它需要理解其体系和最佳实践。4.1 异常体系Error vs. ExceptionThrowable是所有错误和异常的父类。它有两个直接子类Error和Exception。Error表示JVM本身的严重错误如OutOfMemoryError,StackOverflowError,VirtualMachineError。应用程序通常无法处理也不应该捕获这些错误它们意味着程序该终止了。Exception程序运行时可能出现的异常情况可以被捕获和处理。它又分为受检异常继承自Exception但不继承RuntimeException。编译器强制要求处理要么try-catch要么在方法签名中用throws声明。如IOException,SQLException。代表“可预期的异常情况”。非受检异常继承自RuntimeException。编译器不强制要求处理。如NullPointerException,IllegalArgumentException,ArrayIndexOutOfBoundsException。通常代表编程错误空指针、下标越界或参数不合法。设计哲学受检异常用于可恢复的、期望调用者处理的情况如文件未找到可能提示用户重选。非受检异常用于程序错误通常意味着代码有bug应该修复代码而不是捕获。4.2 try-with-resources优雅的资源管理在Java 7之前关闭资源如InputStream,Connection,Socket的代码非常冗长且容易遗漏通常放在finally块中。try-with-resources语句彻底简化了这一切。// 老式写法 BufferedReader br null; try { br new BufferedReader(new FileReader(file.txt)); // ... 使用br } catch (IOException e) { // 处理异常 } finally { if (br ! null) { try { br.close(); } catch (IOException e) { /* 忽略或记录 */ } } } // try-with-resources 写法 (Java 7) try (BufferedReader br new BufferedReader(new FileReader(file.txt))) { // ... 使用br } catch (IOException e) { // 处理异常 }原理在try后的括号中声明的资源必须实现AutoCloseable接口它只有一个close()方法。无论try块是正常结束还是异常退出JVM都会自动调用这些资源的close()方法并且后声明的资源会先关闭。如果try块和close()都抛出了异常try块的异常会被抑制可以通过Throwable.getSuppressed()方法获取被抑制的异常通常是close()抛出的。4.3 异常处理的最佳实践与反模式只捕获你能处理的异常不要用catch (Exception e)来捕获所有异常然后什么都不做“吞掉异常”或只打印堆栈。这会让问题在沉默中恶化。提供有意义的异常信息抛出自定义异常或包装异常时务必包含清晰的错误信息和可能的原因。throw new IllegalArgumentException(参数userId不能为null当前值 userId);优先使用非受检异常现代框架如Spring更倾向于使用非受检异常。这减少了代码的侵入性不需要到处写throws将“是否处理”的选择权交给调用者。对于确实需要调用者知晓的“业务异常”可以定义自己的BusinessException继承RuntimeException。避免在finally块中return这会导致try或catch块中抛出的异常被覆盖丢失关键的调试信息。异常性能创建异常对象尤其是填充堆栈轨迹是一个相对昂贵的操作。不要用异常来控制正常的程序流程比如用throw来跳出循环。5. 并发编程基石线程、锁与内存模型并发是Java面试和高级开发绕不开的坎。理解其基础比盲目使用高级并发工具更重要。5.1 线程的生命周期与基本操作线程状态在Thread.State枚举中定义NEW已创建但未启动start()。RUNNABLE正在JVM中执行或等待操作系统CPU调度。BLOCKED等待获取一个监视器锁synchronized锁以进入同步块/方法。这是与其他线程竞争锁时的状态。WAITING无限期等待直到被其他线程显式唤醒。调用Object.wait(),Thread.join(),LockSupport.park()会进入此状态。TIMED_WAITING有限时间的等待。调用Thread.sleep(long),Object.wait(long),Thread.join(long)等。TERMINATED线程已执行完毕。关键方法辨析start()vsrun()start()会启动新线程在新线程中调用run()方法。直接调用run()只是在当前线程执行该方法不会启动新线程。sleep()vswait()sleep()是Thread的静态方法让当前线程休眠不释放锁。wait()是Object的实例方法必须在synchronized块内调用会让当前线程等待并释放锁直到其他线程调用同一对象的notify()/notifyAll()。yield()提示调度器当前线程愿意让出CPU但调度器可以忽略此提示。不能依赖它来做线程同步。5.2 synchronized关键字内置锁的细节synchronized是Java最基本的互斥同步手段。它可以修饰实例方法、静态方法、代码块。锁对象修饰实例方法锁是当前实例对象this。修饰静态方法锁是当前类的Class对象。修饰代码块锁是synchronized(obj)括号里配置的对象。可重入性同一个线程可以多次获取同一把锁。JVM会维护一个计数器获取时1释放时-1减到0时其他线程才能获取。这避免了线程自己把自己锁死。内存语义synchronized不仅能保证原子性还能保证可见性。线程在进入同步块时会清空工作内存从主内存重新读取共享变量。退出同步块时会把工作内存中修改过的变量刷新回主内存。这遵循了Java内存模型JMM的happens-before规则。局限性synchronized是独占锁性能开销相对较大且无法中断一个正在等待锁的线程也无法尝试获取锁获取不到就阻塞锁的释放必须由获得锁的线程在同步块结束时进行。这些局限性催生了java.util.concurrent.locks.Lock接口。5.3 volatile关键字轻量级的同步volatile是比synchronized更轻量的同步机制但它只能保证可见性和有序性不能保证原子性。可见性当一个线程修改了volatile变量的值新值会立即被刷新到主内存。当其他线程读取该变量时会从主内存中读取最新的值而不是使用自己工作内存中的旧值。有序性禁止指令重排序。普通变量仅保证在方法执行过程中所有依赖赋值结果的地方能获取到正确结果但不保证变量赋值操作的顺序与程序代码中的执行顺序一致指令重排序优化。volatile变量会在此变量前后插入内存屏障防止其前后的指令被重排序。典型用法状态标志private volatile boolean running;一个线程通过running false;来通知另一个线程停止。另一个线程循环检查while (running) { ... }。单例模式的双重检查锁定public class Singleton { private static volatile Singleton instance; // 必须volatile private Singleton() {} public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // 非原子操作可能发生指令重排 } } } return instance; } }这里volatile的关键作用是防止指令重排。instance new Singleton();这行代码不是原子操作它分为1) 分配内存2) 初始化对象3) 将引用指向内存地址。如果没有volatile步骤2和3可能被重排导致其他线程在第一次检查时拿到一个未完全初始化的对象。volatile禁止了这种重排。5.4 Java内存模型与happens-before原则Java内存模型定义了线程如何以及何时可以看到其他线程修改过的共享变量以及如何同步地访问共享变量。其核心是happens-before原则它定义了操作之间的偏序关系保证了内存可见性。几条重要的happens-before规则程序次序规则同一个线程中前面的操作happens-before于后面的操作。管程锁定规则一个unlock操作happens-before于后续对同一个锁的lock操作。volatile变量规则对一个volatile变量的写操作happens-before于后续对这个变量的读操作。线程启动规则Thread.start()happens-before于该线程的每一个动作。线程终止规则线程中的所有操作都happens-before于其他线程检测到该线程已经终止通过Thread.join()或Thread.isAlive()返回false。传递性如果A happens-before B且B happens-before C那么A happens-before C。理解happens-before就能理解为什么在某些情况下不加锁也能保证可见性比如通过volatile变量或线程启动/终止规则这是构建高效并发程序的理论基础。6. I/O与NIO从阻塞到非阻塞的演进Java的I/O体系庞大理解其演进脉络比死记API更重要。6.1 传统BIO流的世界Java传统的I/O基于“流”Stream它是单向的。核心抽象是InputStream和OutputStream字节流以及它们的装饰器Reader和Writer字符流基于字节流和字符集编码。关键点装饰器模式这是Java I/O库的经典设计模式。BufferedInputStream、DataInputStream等本身也是InputStream它们包装另一个InputStream为其增加缓冲、读取基本数据类型等功能。这种设计非常灵活。资源关闭必须关闭流否则会导致文件句柄或网络连接泄漏。务必使用try-with-resources。缓冲的重要性直接读取单个字节/字符性能极差。务必使用缓冲流BufferedInputStream/BufferedReader来包装底层流它们内部维护了一个缓冲区减少了实际的系统调用次数。BIO的瓶颈传统的ServerSocket编程是阻塞式的。当一个线程调用ServerSocket.accept()、socket.read()时如果连接或数据没有就绪线程会被挂起直到事件就绪。这意味着每个连接都需要一个独立的线程来处理。在连接数很高时线程上下文切换的开销巨大系统资源被迅速耗尽。这就是著名的C10K问题。6.2 NIO面向缓冲区的非阻塞I/OJava NIONew I/O在Java 1.4引入解决了BIO的线程模型问题。它的核心是通道Channel、缓冲区Buffer和选择器Selector。Buffer一个可以读写数据的容器。所有数据都通过Buffer处理。有ByteBuffer,CharBuffer等。关键属性容量capacity、位置position、限制limit。Channel类似于流但可以双向读写且必须和Buffer配合使用。主要实现有FileChannel、SocketChannel、ServerSocketChannel。Selector多路复用器。一个Selector可以轮询多个Channel上的事件连接就绪、读就绪、写就绪。当某个Channel有事件就绪时Selector会通知应用程序然后由应用程序处理。一个线程可以管理多个Channel这是实现高并发的关键。NIO的工作模式创建Selector并将需要监听的Channel注册到Selector上指定感兴趣的事件SelectionKey.OP_ACCEPT,OP_READ,OP_WRITE。调用Selector.select()它会阻塞直到有注册的事件发生。获取发生事件的SelectionKey集合遍历处理。在处理OP_ACCEPT事件时接受连接并将新的SocketChannel也注册到Selector。在处理OP_READ事件时从Channel读取数据到Buffer进行处理。NIO的复杂性NIO的API比BIO复杂得多需要手动管理Buffer的状态flip, clear, compact并且网络编程需要处理半包、粘包等问题。虽然解决了线程模型问题但编程模型变得复杂这也是后来Netty等框架流行的原因——它们封装了NIO的复杂性提供了更易用的API。6.3 NIO.2真正的异步I/OJava 7引入了NIO.2主要提供了AsynchronousFileChannel和AsynchronousSocketChannel支持真正的异步I/O操作AIO。其核心是Future和CompletionHandler两种编程模式。Future模式调用异步方法立即返回一个Future对象后续可以通过Future.get()阻塞等待结果或轮询Future.isDone()。AsynchronousFileChannel channel AsynchronousFileChannel.open(path); ByteBuffer buffer ByteBuffer.allocate(1024); FutureInteger future channel.read(buffer, 0); // 立即返回 // ... 做其他事情 Integer bytesRead future.get(); // 阻塞直到读取完成CompletionHandler模式调用异步方法时传入一个回调接口CompletionHandler当操作完成成功或失败时会调用对应的回调方法。channel.read(buffer, 0, buffer, new CompletionHandlerInteger, ByteBuffer() { Override public void completed(Integer result, ByteBuffer attachment) { // 读取成功处理数据 } Override public void failed(Throwable exc, ByteBuffer attachment) { // 读取失败处理异常 } });AIO理论上性能更高因为它将I/O操作完全交给操作系统应用线程无需等待。但在Linux上底层实现仍可能使用epoll模拟且编程模型复杂在实际生产环境中特别是网络编程的应用不如基于NIO的Netty广泛。在文件I/O场景下AIO能发挥一定优势。7. JVM基础程序如何运行了解JVM的基本结构是理解Java程序性能、内存问题和异常的基础。7.1 运行时数据区JVM在执行Java程序时会把它管理的内存划分为若干个不同的区域。程序计数器线程私有。指向当前线程正在执行的字节码指令的地址。分支、循环、跳转、异常处理都依赖它。Java虚拟机栈线程私有。生命周期与线程相同。每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。我们常说的“栈内存”就是指这里。局部变量表存放基本数据类型和对象引用。本地方法栈与虚拟机栈类似但为JVM用到的Native方法服务。Java堆线程共享。几乎所有对象实例和数组都在这里分配内存。是垃圾收集器管理的主要区域因此常被称为“GC堆”。从内存回收角度可分为新生代Eden, Survivor0, Survivor1和老年代。方法区线程共享。存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。在HotSpot VM中方法区常被称为“永久代”但在Java 8中永久代被移除取而代之的是元空间它使用本地内存不再受JVM最大堆参数限制。运行时常量池方法区的一部分。存放编译期生成的各种字面量和符号引用。7.2 垃圾回收的基本思想垃圾回收GC的目标是回收堆内存中不再使用的对象。核心问题是如何判断对象“不再使用”引用计数法给对象添加一个引用计数器有引用时1引用失效时-1为0时可回收。简单但无法解决循环引用问题A引用BB引用A但外部再无引用它们。可达性分析算法Java采用的方法。以一系列称为“GC Roots”的对象作为起始点向下搜索搜索走过的路径称为“引用链”。当一个对象到GC Roots没有任何引用链相连时则证明此对象不可用。GC Roots包括虚拟机栈中引用的对象、本地方法栈中JNI引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象等。即使通过可达性分析不可达对象也并非“非死不可”。它会被第一次标记并进行一次筛选判断是否有必要执行finalize()方法。如果对象重写了finalize()且未被JVM调用过它会被放入一个队列由低优先级的Finalizer线程去执行它的finalize()方法。注意finalize()方法运行代价高不确定性大无法保证调用顺序不推荐使用。释放资源请用try-with-resources或显式调用close()。7.3 常见的垃圾收集器了解不同收集器的特点有助于在特定场景下进行JVM调优。Serial / Serial Old单线程收集器。进行GC时必须暂停所有其他工作线程“Stop The World”。简单高效是Client模式下的默认新生代收集器。ParNewSerial的多线程版本是许多Server模式下的首选新生代收集器因为只有它能与CMS收集器配合工作。Parallel Scavenge / Parallel OldJDK 8默认的收集器组合。目标是达到一个可控制的吞吐量运行用户代码时间 / (运行用户代码时间 GC时间)。适合后台运算、不需要太多交互的任务。CMS以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法过程分为初始标记、并发标记、重新标记、并发清除。其中初始标记和重新标记仍需“Stop The World”。它是一款优秀的收集器但会产生内存碎片且对CPU资源敏感。G1JDK 9及之后的默认收集器。它将堆划分为多个大小相等的独立区域跟踪各个区域垃圾堆积的价值大小在后台维护一个优先列表优先回收价值最大的区域。是一款面向服务端应用的收集器能同时兼顾低停顿和高吞吐量。ZGC / Shenandoah新一代的低延迟垃圾收集器目标是将停顿时间控制在10ms以内适用于超大堆内存TB级别的场景。对于大多数应用使用JDK 8的默认Parallel收集器或升级到JDK 11使用G1收集器并合理设置堆大小-Xms,-Xmx和新生代比例-XX:NewRatio通常就能获得不错的性能。深入调优需要结合具体的应用特点和监控数据。8. 新特性掠影从Java 8到Java 11虽然我们聚焦“经典”但了解关键的新特性演进是必要的。这里重点提一下Java 8和Java 11中影响最深远的几个特性。8.1 Java 8函数式编程与Stream APIJava 8是近年来最重要的更新它引入了Lambda表达式和Stream API彻底改变了Java的编程风格。Lambda表达式本质是一个匿名函数允许把函数作为一个方法的参数。语法(parameters) - expression或(parameters) - { statements; }。它使得行为参数化变得非常简洁是函数式接口只有一个抽象方法的接口的实例。函数式接口使用FunctionalInterface注解标注如Runnable,Comparator, 以及Java 8新增的PredicateT,FunctionT,R,ConsumerT,SupplierT等。Lambda表达式极大地简化了匿名内部类的书写。Stream API用于处理集合数据的声明式编程模型。它允许你以声明的方式处理数据并可以透明地进行并行处理。ListString names Arrays.asList(Alice, Bob, Charlie, David); ListString result names.stream() .filter(name - name.length() 3) // 中间操作 .map(String::toUpperCase) // 中间操作 .sorted() // 中间操作 .collect(Collectors.toList()); // 终端操作Stream操作分为中间操作返回Stream可链式调用如filter, map, sorted和终端操作产生结果或副作用如collect, forEach, count。Stream是惰性求值的只有终端操作被调用时中间操作才会执行。8.2 Java 9-11模块化与实用增强Java 9 模块化引入了JPMS允许开发者将代码组织成明确的模块通过module-info.java声明模块的依赖和导出。目的是解决“类路径地狱”增强封装性和安全性。但对于大多数应用级开发影响不大更多是库和框架开发者需要关注。Java 10 局部变量类型推断引入了var关键字允许在声明局部变量时省略显式类型由编译器推断。var list new ArrayListString(); // 推断为 ArrayListString var stream list.stream(); // 推断为 StreamStringvar只能用于局部变量必须有初始化器不能用于方法参数、返回类型、字段等。它让代码更简洁但需谨慎使用避免降低可读性。Java 11 HTTP Client提供了一个全新的、支持HTTP/2和WebSocket的HTTP Client API用于替代老旧的HttpURLConnection。它是异步的且API更现代、易用。HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://example.com)) .build(); // 同步 HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); // 异步 CompletableFutureHttpResponseString future client.sendAsync(request, HttpResponse.BodyHandlers.ofString());Java 11 单文件源代码程序允许直接运行单个.java文件无需先编译。java HelloWorld.java。这对于学习和小脚本非常方便。回顾这份2021年的基础总结你会发现它涵盖的正是Java语言中最稳定、最核心、最经得起时间考验的部分。无论框架如何变迁这些基础构成了你理解一切上层建筑的基石。扎实掌握它们你就能更快地理解新特性更稳地排查复杂问题写出更高质量的代码。技术总是在更新但底层的原理和思想往往历久弥新。
返回列表