ARTICLE DETAIL

资讯详情

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

刚刚 Java 25 炸裂发布!让 Java 再次伟大

刚刚 Java 25 炸裂发布!让 Java 再次伟大

大家好,我是程序员鱼皮。

刚刚,Java 25 正式发布!这是继 Java 21 之后,又一个 LTS 长期支持版本,也是 Java 开发者们最期待的版本之一。其中有个特性可以说是颠覆了我对 Java 的认知,让 Java 再次伟大!

那么 Java 25 都发布了哪些新特性?有没有必要升级?

一篇文章,带你速通 Java 新特性,学会后又能愉快地和面试官吹牛皮了~

推荐观看视频版:https://bilibili.com/video/BV1b5pCzGEPx

⭐️ 正式特性

这些特性在 Java 25 中正式稳定,可以在生产环境中放心使用。

【实用】Scoped Values 作用域值

如果我问你:怎么在同一个线程内共享数据?

估计你的答案是 ThreadLocal。

但是你有没有想过,ThreadLocal 存在什么问题?

举一个典型的 ThreadLocal 使用场景,在同一个请求内获取用户信息:

public class UserService {
   private static final ThreadLocal<StringUSER_ID new ThreadLocal<>();
   
   public void processRequest(String userId) {
       USER_ID.set(userId);  // 写入
       doWork();
       USER_ID.remove();  // 问题:必须手动清理,容易忘记
  }
   
   public void doWork() {
       String userId USER_ID.get();  // 读取
       System.out.println("处理用户: " userId);
       // 问题:其他代码可以随意修改
       USER_ID.set("被篡改的值");
  }
}

这段简单的代码其实暗藏玄鸡,可以看出 ThreadLocal 的痛点:

  1. 容易内存泄漏:必须手动调用 remove()

  2. 可以随意修改数据,可能导致不可预期的结果

此外,如果想让子线程也共享数据,每个子线程都要复制一份数据。如果你使用的是 Java 21 的虚拟线程,1000 个虚拟线程就要复制1000 次,性能很差。

InheritableThreadLocal<StringthreadLocal new InheritableThreadLocal<>();
threadLocal.set("用户数据");
for (int 0; 1000; i++) {
   Thread.ofVirtual().start(() -> {
       String data threadLocal.get(); // 每个线程都有自己的副本
  });
}

现在 Java 25 的 Scoped Values 特性转正了,能解决 ThreadLocal 的这些问题。

 

什么是 Scoped Values?

Scoped Values 允许方法 在线程内以及子线程间安全高效地共享不可变数据

和传统的 ThreadLocal 相比,它不仅更安全,而且在虚拟线程环境下的内存开销要小很多。

Scoped Values 和 ThreadLocal 的写法很像:

import static java.lang.ScopedValue.where;

public class UserService {
   private static final ScopedValue<StringUSER_ID ScopedValue.newInstance();
   
   public void processRequest(String userId) {
       where(USER_ID, userId)  // 写入并绑定作用域
          .run(this::doWork);
       // 自动清理,无需 remove()
  }
   
   public void doWork() {
       String userId USER_ID.get();  // 读取
       System.out.println("处理用户: " userId);
  }
}

这段代码中,我们使用 where().run() 自动管理作用域,出了作用域就自动清理,更安全。

而且作用域一旦绑定,值就不能被修改,避免意外的状态变更。

和虚拟线程配合使用时,所有虚拟线程共享同一份数据,内存占用更小:

ScopedValue<StringscopedValue ScopedValue.newInstance();
where(scopedValue, "用户数据").run(() -> {
   for (int 0; 1000; i++) {
       Thread.ofVirtual().start(() -> {
           String data scopedValue.get(); // 所有线程共享同一份数据
      });
  }
});

 

使用方法

1)支持返回值

除了 run() 方法,还可以使用 call() 方法来处理有返回值的场景:

public String processWithResult(String input) {
   return where(CONTEXT, input)
      .call(() -> {
           String processed doSomeWork();
           return "结果: " processed;
      });
}

 

2)嵌套作用域

支持在已有作用域内建立新的嵌套作用域:

void outerMethod() {
   where(X, "hello").run(() -> {
       System.out.println(X.get());  // 输出 "hello"
       where(X, "goodbye").run(() -> {
           System.out.println(X.get());  // 输出 "goodbye"
      });
       System.out.println(X.get());  // 输出 "hello"
  });
}

 

3)多值绑定

可以在一个调用中绑定多个 Scoped Values,或者直接用类封装多个值:

where(USER_ID, userId)
  .where(REQUEST_ID, requestId)
  .where(TENANT_ID, tenantId)
  .run(() -> {
       processRequest();
  });

 

4)和结构化并发配合

Scoped Values 和 Java 结构化并发 API 可以打个配合,子线程自动继承父线程的作用域值:

void handleRequest() {
   where(USER_ID, getCurrentUserId())
      .run(() -> {
           try (var scope new StructuredTaskScope.ShutdownOnFailure()) {
               var userTask scope.fork(() -> loadUser());      // 子线程可以访问 USER_ID
               var ordersTask scope.fork(() -> loadOrders());  // 子线程可以访问 USER_ID
               scope.join().throwIfFailed();
               return new Response(userTask.get(), ordersTask.get());
          }
      });
}

不过结构化并发这哥们也挺惨的,过了这么多个版本还没转正。等它转正了,感觉 Java 并发编程的模式也要改变了。

 

使用场景

虽然 Scoped Values 听起来比 ThreadLocal 更高级,但它不能 100% 替代 ThreadLocal。

如果你要在线程中共享不可变数据、尤其是使用了虚拟线程的场景,建议使用 Scoped Values;但如果线程中共享的数据可能需要更新,那么还是使用 ThreadLocal,要根据实际场景选择。

 

【实用】模块导入声明

模块导入声明特性(Module Import Declarations)虽然是首次亮相,但它的设计理念可以追溯到 Java 9 的模块系统。

模块系统允许我们将代码组织成模块,每个模块都有明确的依赖关系和导出接口,让大型应用的架构变得更加清晰和可维护。

模块导入声明是在这个基础上进一步简化开发体验。

以前我们使用多个 Java 标准库的包需要大量的导入语句:

import java.util.Map;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.function.Function;
import java.nio.file.Path;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
// ... 还有更多

 

现在可以一行导入整个模块:

import module java.base;

 

对于聚合模块(比如 java.se),一次导入可以使用大量包:

import module java.se;  // 导入 100 多个包

public class FullFeatureApp {
   // 可以使用整个 Java SE 平台的所有公开 API
}

 

不过我感觉这个特性会比较有争议,我记得大厂的 Java 规约中是禁止使用通配符 * 方式导入所有类的,可读性差、有命名冲突风险、依赖不明确。

而模块导入的范围更大,类名冲突可能更严重。如果导入多个模块时遇到了同名类,还要再通过具体导入来解决:

import module java.base;      // 包括 java.util.List 接口
import module java.desktop;   // 包括 java.awt.List 类

import java.util.List;        // 明确指定使用 util 包的 List

所以我可能不会用这个特性,纯个人偏好。

 

【必备】紧凑源文件和实例主方法

这是我最喜欢的特性,直接打破了外界对于 Java 的刻板印象!

之前不是都说 Java 入门比 Python 难么?一个简单的 Hello World 程序就要包含类、public、static、方法参数等概念。

传统的 Hello World 程序:

public class HelloWorld {
   public static void main(String[] args) {
       System.out.println("Hello, World!");
  }
}

用 Python 或者 JavaScript 直接写一行代码就完成了:

print("Hello World")

在 Java 25 中,Hello World 程序可以直接简写为 3 行代码!

void main() {
   IO.println("Hello, World!");
}

这么一看和好多语言都有点像啊。。。安能辨我是 Java?

但是你知道,这 3 含代码的含金量么?你知道 Java 为了支持简写成这 3 行代码付出了多少努力么?

 

新的 IO 类

首先是 Java 25 在 java.lang 包中新增了 IO 类,提供更简单的控制台 I/O 操作:

void main() {
   String name IO.readln("请输入您的姓名: ");
   IO.print("很高兴认识您,");
   IO.println(name);
}

 

IO 类的主要方法包括:

public static void print(Object obj);
public static void println(Object obj);
public static void println();
public static String readln(String prompt);
public static String readln();

 

自动导入 java.base 模块

在紧凑源文件中,所有 java.base 模块导出的包都会自动导入,就像有这样一行代码:

import module java.base;

也就是说,你可以直接使用 List、Map、Stream 等常用类:

void main() {
   var fruits List.of("苹果", "香蕉", "橙子");
   var lengths fruits.stream()
      .collect(Collectors.toMap(
           fruit -> fruit,
           String::length
      ));
   IO.println("水果及其长度: " lengths);
}

这样一来,Java 对初学者更友好,同时也让有经验的开发者能快速编写脚本和小工具。

我觉得这个特性是让 Java 再次伟大的关键,为什么呢?

Java 的核心竞争优势在于它成熟完善的生态系统,但语法不够简洁;现在 Java 只要持续借鉴其他新兴编程语言的优秀设计和语法特性、给开发者提供平滑的技术升级路径,就还是会有很多开发者继续使用 Java,就不会被别的语言取代。

 

【实用】灵活的构造函数体

这个特性解决的是 Java 自诞生以来就存在的一个限制:构造函数中的 super()this() 调用必须是第一条语句。

这个限制虽然保证了对象初始化的安全性,但可能也会影响我们的编码。

举个例子:

class Employee extends Person {
   Employee(String name, int age) {
       super(name, age);  // 必须是第一条语句
       if (age 18 || age 67) {
           throw new IllegalArgumentException("员工年龄不符要求");
      }
  }
}

这种写法的问题是,即使参数不符合要求,也会先调用父类构造函数,做一些可能不必要的工作。

Java 25 打破了这个限制,引入了新的构造函数执行模型,分为两个阶段:

  1. 序言阶段:构造函数调用之前的代码

  2. 尾声阶段:构造函数调用之后的代码

简单来说,允许在构造函数调用之前添加语句!

class Employee extends Person {
   String department;
   
   Employee(String name, int age, String department) {
       // 序言阶段,可以校验参数
       if (age 18 || age 67) {
           throw new IllegalArgumentException("员工年龄必须在 18-67 之间");
      }        
       // 可以初始化字段
       this.department department;
       // 验证通过后,再调用父类构造函数
       super(name, age);
       // 尾声阶段,可以干点儿别的事
       IO.println("新员工 " name " 已加入 " department " 部门");
  }
}

怎么样,感觉是不是一直以来背的八股文、做的选择题崩塌了!

此外,这个特性还能防止父类构造函数调用子类未初始化的方法:

class Base {
   Base() {
       this.show();  // 调用可能被重写的方法
  }
   void show() {
       IO.println("Base.show()");
  }
}

class Yupi extends Base {
   private String message;
   
   Yupi(String message) {
       this.message message;  // 在 super() 之前初始化
       super();
  }
   
   @Override
   void show() {
       IO.println("消息: " message);  // message 已经正确初始化了
  }
}

现在,当创建 Yupi 对象时,message 字段会在父类构造函数运行之前就被正确初始化,避免了打印 null 值的问题。

 

限制条件

但是要注意,在序言阶段有一些限制:

  1. 不能使用 this 引用(除了字段赋值)

  2. 不能调用实例方法

  3. 只能对未初始化的字段进行赋值

class Example {
   int value;
   String name "default";  // 有初始化器
   
   Example(int val, String nm) {
       // ✅ 允许:验证参数
       if (val 0) throw new IllegalArgumentException();
       
       // ✅ 允许:初始化未初始化的字段
       this.value val;
       
       // ❌ 不允许:字段已有初始化器
       // this.name = nm;
       
       // ❌ 不允许:调用实例方法
       // this.helper();
       
       super();
       
       // ✅ 现在可以正常使用 this 了
       this.name nm;
       this.helper();
  }
   
   void helper() { /* ... */ }
}

总之,这个特性让构造函数变得更加灵活和安全,特别适合需要复杂初始化逻辑的场景。

 

【了解】密钥派生函数 API

随着量子计算技术的发展,传统的密码学算法面临威胁,后量子密码学成为必然趋势。

因此 Java 也顺应时代,推出了密钥派生函数(KDF),这是一种从初始密钥材料、盐值等输入生成新密钥的加密算法。

简单来说,你理解为 Java 出了一个新的加密工具类就好了,适用于对密码进行加强、从主密钥派生多个子密钥的场景。

核心是 javax.crypto.KDF 类,提供了两个主要方法:

  1. deriveKey() 生成 SecretKey 对象

  2. deriveData() 生成字节数组

比如使用 HKDF(HMAC-based Key Derivation Function)算法:

// 创建 HKDF 实例
KDF hkdf KDF.getInstance("HKDF-SHA256");

// 准备初始密钥材料和盐值
byte[] initialKeyMaterial "my-secret-key".getBytes();
byte[] salt "random-salt".getBytes();
byte[] info "application-context".getBytes();

// 创建 HKDF 参数
AlgorithmParameterSpec params HKDFParameterSpec.ofExtract()
  .addIKM(initialKeyMaterial)  // 添加初始密钥材料
  .addSalt(salt)               // 添加盐值
  .thenExpand(info, 32);       // 扩展为 32 字节

// 派生 AES 密钥
SecretKey aesKey hkdf.deriveKey("AES", params);

// 或者直接获取字节数据
byte[] derivedData hkdf.deriveData(params);

 

【了解】紧凑对象头

了解过 Java 对象结构的同学应该知道,Java 对象除了存储数据外,还要通过 对象头 存储很多额外的信息,比如类型信息、GC 标记、锁状态等元数据。

 

如果程序中要创建大量小对象,可能对象头本身占用的空间都比实际要存储的数据多了!

比如下面这段代码:

class Point {
   int x, y;  // 实际数据只有 8 字节
}

// 创建一堆 Point 对象
List<Pointpoints new ArrayList<>();
for (int 0; 10000; i++) {
   points.add(new Point(i, i));  // 每个对象实际占用 24 字节!
}

用来存储传统的对象头在 64 位系统上占 16 字节,对于只有 8 字节数据的 Point 来说,开销确实有点大。

Java 25 将紧凑对象头特性转正,把对象头从 16 字节压缩到 8 字节,减少了小对象的内存开销。

但是,紧凑对象头并不是默认开启的,需要手动指定。毕竟少存了一些信息,必须考虑到兼容性,比如下面这几种情况可能会有问题:

  • 使用了 JNI 直接操作对象头的代码

  • 依赖特定对象头布局的调试工具

  • 某些第三方性能分析工具

 

【了解】Shenandoah 分代收集

对于 Java 这种自动垃圾收集的语言,我们经常遇到这种问题:GC 一运行,应用就卡顿。特别是占用大堆内存的应用,传统的垃圾收集器一跑起来,停顿时间可能多达几百毫秒甚至几秒。

Shenandoah 作为 Java 中延迟最低的垃圾收集器,在 Java 25 中 Shenandoah 的分代模式转为正式特性。

什么是分代垃圾回收呢?

大部分对象都是 “朝生夕死” 的。将堆内存划分为年轻代和老年代两个区域,年轻代的垃圾收集可以更加频繁和高效,因为大部分年轻对象很快就会死亡,收集器可以快速清理掉这些垃圾;而老年代的收集频率相对较低,减少了对长期存活对象的不必要扫描。

经过大量测试,分代 Shenandoah 在保持超低延迟的同时,还能获得更好的吞吐量。对于延迟敏感的应用来说,这个改进还是很实用的。

但是 Shenandoah 并不是默认的垃圾收集器,需要手动指定。而且分代模式也不是 Shenandoah 的默认模式,默认情况下 Shenandoah 还是使用单代模式。

 

预览特性

这些特性仍在预览阶段,可以体验 但不建议在生产环境使用

【了解】结构化并发

目前我们经常使用 ExecutorService 实现并发,可能会写下面这种代码:

// 传统写法
Response handle() throws ExecutionException, InterruptedException {
   Future<Stringuser executor.submit(() -> findUser());
   Future<Integerorder executor.submit(() -> fetchOrder());
   
   String theUser user.get();   // 如果这里异常了
   int theOrder order.get();    // 这个任务还在跑,造成线程泄漏!
   
   return new Response(theUser, theOrder);
}

上述代码存在一个问题,如果 findUser() 方法失败了,fetchOrder() 还在后台运行,浪费资源。而且如果当前线程被中断,子任务不会自动取消。

 

什么是结构化并发?

结构化并发就是来解决并发编程中 线程泄露和资源管理 问题的。它在 Java 25 中第 5 次预览,API 已经成熟,盲猜下个版本就要转正了。

结构化并发的基本使用方法:

Response handle() throws InterruptedException {
   // 打开新作用域,打开作用域的线程是作用域的所有者。
   try (var scope StructuredTaskScope.open()) {
       // 使用 fork 方法在作用域中开启子任务
       var userTask scope.fork(() -> findUser());
       var orderTask scope.fork(() -> fetchOrder());
       // 等待所有子任务完成或失败
       scope.join();
       return new Response(userTask.get(), orderTask.get());
  }
   // 作用域结束时,所有子任务自动清理,不会泄漏
}

这样就解决了几个问题:

  • 自动清理:任一任务失败,其他任务自动取消

  • 异常传播:主线程被中断,子任务也会取消

  • 资源管理:可以配合 try-with-resources 保证资源释放

 

更多用法

Java 25 提供了多种执行策略:

// 1. 默认策略:所有任务都要成功
try (var scope StructuredTaskScope.open()) {
   // 任一失败就全部取消
}

// 2. 竞速策略:任一成功即可
try (var scope StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow())) {
   var task1 scope.fork(() -> callService1());
   var task2 scope.fork(() -> callService2());
   var task3 scope.fork(() -> callService3());
   
   // 谁先成功就用谁的结果,其他任务取消
   return scope.join();
}

// 3. 收集所有结果(忽略失败)
try (var scope StructuredTaskScope.open(Joiner.awaitAll())) {
   // 等待所有任务完成,不管成功失败
}

结构化并发搭配虚拟线程,可以轻松处理大量并发:

void processLotsOfRequests() throws InterruptedException {
   try (var scope StructuredTaskScope.open()) {
       // 创建上万个虚拟线程都没问题
       for (int 0; 10000; i++) {
           int requestId i;
           scope.fork(() -> processRequest(requestId));
      }
       scope.join();  // 等待所有请求处理完成
  }
   // 自动清理,不用担心线程泄漏
}

 

【了解】基本类型模式匹配

之前 Java 优化过很多次模式匹配,可以利用 switch 和 instanceof 快速进行类型检查和转换。

比如:

public String processMessage(Object message) {
   return switch (message) {
       case String text -> "文本消息:" text;
       case Integer number -> "数字消息:" number;
       case List<?> list -> "列表消息,包含 " list.size() " 个元素";
       case  -> "空消息";
       default -> "未知消息类型";
  };
}

但是目前模式匹配只能用于引用类型,如果你想在 switch 中匹配基本类型,只能用常量,不能绑定变量。

支持基本类型模式匹配后,switch 中可以使用基本类型:

switch (value) {
   case int when 100 -> "大整数: " i;
   case int -> "小整数: " i;
   case float -> "浮点数: " f;
   case double -> "双精度: " d;
}

注意这里的 int i,变量 i 绑定了匹配的值,可以直接使用。

而且基本类型模式会检查转换是否安全,这比手动写范围检查方便多了!

int largeInt 1000000;

// 检查能否安全转换为 byte
if (largeInt instanceof byte b) {
   IO.println("可以安全转换为 byte: " b);
else {
   IO.println("转换为 byte 会丢失精度");  // 这个会执行
}

// 检查 int 转 float 是否会丢失精度
int preciseInt 16777217;  // 2^24 + 1
if (preciseInt instanceof float f) {
   IO.println("转换为 float 不会丢失精度");
else {
   IO.println("转换为 float 会丢失精度");  // 这个会执行
}

 

【了解】Stable Values 稳定值

这个特性对大多数开发者来说应该是没什么用的。

不信我先考考大家:final 字段有什么问题?

答案是必须在构造时初始化。

举个例子:

class OrderController {
   private final Logger logger Logger.create(OrderController.class);
}

这段代码中,logger 必须立刻初始化,如果创建 logger 很耗时,所有实例都要等待,可能影响启动性能。

特别是在对象很多、但不是每个都会用到某个字段的场景下,这种强制初始化就很浪费。

但我又想保证不可变性,怎么办呢?

Stable Values 可以解决上述问题,提供 延迟初始化的不可变性

class OrderController {
   private final StableValue<Loggerlogger StableValue.of();
   
   Logger getLogger() {
       // 只在首次使用时初始化,之后直接返回同一个实例
       return logger.orElseSet(() -> Logger.create(OrderController.class));
  }
}

说白了其实就是包一层。。。

还有更简洁的写法,可以在声明时指定初始化逻辑:

class OrderController {
   private final Supplier<Loggerlogger 
       StableValue.supplier(() -> Logger.create(OrderController.class));
   
   void logOrder() {
       logger.get().info("处理订单");  // 自动延迟初始化
  }
}

还支持集合的延迟初始化:

class ConnectionPool {
   // 延迟创建连接池,每个连接按需创建
   private static final List<Connectionconnections 
       StableValue.list(POOL_SIZE, index -> createConnection(index));
   
   public static Connection getConnection(int index) {
       return connections.get(index);  // 第一次访问才创建这个连接
  }
}

重点是,Stable Values 底层使用 JVM 的 @Stable 注解,享受和 final 字段一样的优化(比如常量折叠),所以不用担心性能。

这个特性特别适合:

  • 创建成本高的对象

  • 不是每个实例都会用到的字段

  • 需要延迟初始化但又要保证不可变的场景

没记错的话这个特性应该是首次亮相,对于追求性能的开发者来说还是挺实用的。

 

其他特性

【了解】移除 32 位 x86 支持

Java 25 正式移除了对 32 位 x86 架构的支持。

简单来说就是:32 位系统现在用不了 Java 25 了。

不过对绝大多数朋友来说,这个变化应该没啥影响。

 

【了解】JFR 性能分析增强

JFR(Java Flight Recorder)飞行记录器是 JDK 内置的低开销性能监控工具,可以记录程序运行时的详细数据。

它在这个版本获得了几个重要增强,包括更精确地测量 CPU 使用情况、通过协作式采样保证了 JVM 安全性、可以更安全地在生产环境开启分析等。

 

【了解】Vector API(第 10 次孵化)

Vector API 继续孵化,主要改进:

  1. 更好的数学函数支持:现在通过 FFM API 调用本地数学库,提高可维护性

  2. Float16 支持增强:在支持的 CPU 上可以自动向量化 16 位浮点运算

  3. VectorShuffle 增强:支持与 MemorySegment 交互

这个 API 对高性能计算、机器学习等领域很重要,尤其是现在 AI 的发展带火了向量运算。但是我想说真的别再孵化了,等你转正了,黄花菜都凉了。

 


 

以上就是 Java 25 的新特性了,不知道大家有什么想法,可以在评论区留言分享。

我的评价是:虽然 Java 25 在开发体验、性能优化和并发编程方面都有进步,雨露均沾,但是目前对于新项目选择 Java 21 就够了,老项目还是可以继续用 Java 8,真没必要升级到 Java 25。

但是建议大家还是把 Java 8 之后的 Java 新特性都了解一下,拓宽下知识面,面试的时候也能跟面试官多聊几句。我也把免费 Java 教程和 Java 新特性大全都整理到编程导航上了:

免费 Java 教程 + 新特性大全:https://codefather.cn/course/java

如果本期内容有收获,记得点赞关注三连支持,我们下期见。

更多编程学习资源

  • Java前端程序员必做项目实战教程+毕设网站

  • 程序员免费编程学习交流社区(自学必备)

  • 程序员保姆级求职写简历指南(找工作必备)

  • 程序员免费面试刷题网站工具(找工作必备)

  • 最新Java零基础入门学习路线 + Java教程

  • 最新Python零基础入门学习路线 + Python教程

  • 最新前端零基础入门学习路线 + 前端教程

  • 最新数据结构和算法零基础入门学习路线 + 算法教程

  • 最新C++零基础入门学习路线、C++教程

  • 最新数据库零基础入门学习路线 + 数据库教程

  • 最新Redis零基础入门学习路线 + Redis教程

  • 最新计算机基础入门学习路线 + 计算机基础教程

  • 最新小程序入门学习路线 + 小程序开发教程

  • 最新SQL零基础入门学习路线 + SQL教程

  • 最新Linux零基础入门学习路线 + Linux教程

  • 最新Git/GitHub零基础入门学习路线 + Git教程

  • 最新操作系统零基础入门学习路线 + 操作系统教程

  • 最新计算机网络零基础入门学习路线 + 计算机网络教程

  • 最新设计模式零基础入门学习路线 + 设计模式教程

  • 最新软件工程零基础入门学习路线 + 软件工程教程

返回列表