Java17新特性详解

Java 17 是 2021 年 9 月 14 日正式发布的,距今也已经快2年多了,是一个长期支持(LTS)版本。Java 17 这个版本非常重要,Spring Framework 6.0 和 Spring Boot 3.0 最低支持都是 Java 17,搞Java开发肯定是离不开Spring这个主流的框架。下面这张图是 Oracle 官方给出的 Oracle JDK 支持的时间线,可以看得到Java17支持到2029年9月。

增强Switch

之前的 switch 写法如下:

复制代码
String a = "jdk17";
     switch (a){
         case "jdk8":
             System.out.println("我是jdk8");
             break;
         case "jdk17":
             System.out.println("我是jdk17");
             break;
         default:
             System.out.println("未知");
             break;
     }

从jdk12后可以通过 switch 表达式来进行简化。使用箭头"->",并且不需要每个 case 都 break,大大提高了我们的编写效率,如下所示:

复制代码
switch (a) {
            case "jdk8" -> System.out.println("我是jdk8");
            case "jdk17" -> System.out.println("我是jdk17");
            default -> System.out.println("未知");
        }

输出

复制代码
我是jdk17

匹配多个case

如果是匹配多个case 的话用逗号分开,例如:

复制代码
String a = "jdk17";
        String who = switch (a) {
            case "jdk8","jdk17" -> "我是jdk家族";
            case "spring","spring boot" -> "我是spring家族";
            default -> "未知";
        };
 System.out.println(who);

case 多行

如果你想在case里做不止一件事,比如在返回之前先进行一些计算或者打印操作。可以通过大括号来作为case块,最后的返回值使用关键字 yield 进行返回。

复制代码
String a = "spring";
        String who = switch (a) {
            case "jdk8","jdk17" -> {
                System.out.println(1+1);
                yield "我是jdk家族";
            }
            case "spring","spring boot" -> {
                System.out.println(2+2);
                yield "我是spring家族";
            }
            default -> "未知";
        };
        System.out.println(who);

输出

复制代码
4
我是spring家族

case 匹配对象

以及之前的switch只支持 数值和字符串常量的匹配 ,而现在还支持对对象的类型来进行匹配,例如:

复制代码
Object a = 888;
        String who = switch (a) {
            case null -> "is null";
            case Integer i -> String.format("i is %s",i);
            case String s -> String.format("s is string %s",s);
            default -> a.toString();
        };
        System.out.println(who);

输出

复制代码
i is 888

case 语句返回值

JDK13中引入了yield语句,用于返回值。这意味着,switch表达式(返回值)应该使用yield,switch语句(不返回值)应该使用break。

yield和return的区别在于:return会直接跳出当前循环或者方法,而yield只会跳出当前switch块。

在以前:

复制代码
@Test
public void testSwitch1(){
    String x = "3";
    int i;
    switch (x) {
        case "1":
            i=1;
            break;
        case "2":
            i=2;
            break;
        default:
            i = x.length();
            break;
    }
    System.out.println(i);
}

在JDK13中:

复制代码
@Test
public void testSwitch2(){
    String x = "3";
    int i = switch (x) {
        case "1" -> 1;
        case "2" -> 2;
        default -> {
            yield 3;
        }
    };
    System.out.println(i);
}

或者

复制代码
@Test
public void testSwitch3() {
    String x = "3";
    int i = switch (x) {
        case "1":
            yield 1;
        case "2":
            yield 2;
        default:
            yield 3;
    };
    System.out.println(i);
}

文本块

在Java17之前的版本里,如果我们需要定义一个字符串,比如一个JSON数据或者一些html等,基本都是采用拼接的方式去定义,大量的加号和转义的双引号非常的恶心且难看,例如:

复制代码
String text = "{\n" +
                "  \"name\": \"小黑说Java\",\n" +
                "  \"age\": 18,\n" +
                "  \"address\": \"北京市西城区\"\n" +
                "}";

而从jdk15后这个写法才得以改善,只需要在前后加上 """ 。

复制代码
String text = """
                {
                  "name": "小黑说Java",
                  "age": 18,
                  "address": "北京市西城区"
                }
                """;

显然,更方便,更简洁了。若是在文本块内加入空格或者换行之类的,请参考以下:

复制代码
\s:表示空格
\:表示不换行
\s\:表示添加一个空格且不换行

instanceof增强 -模式匹配

通常我们使用instanceof时,一般发生在需要对一个变量的类型进行判断,如果符合指定的类型,则强制类型转换为一个新变量。

之前的写法:

复制代码
List<?> rows = selectMenuList(menu, getUserId()).getRows();
List<SysMenu> menus =  new ArrayList<>();
rows.forEach(r->{
   if (r instanceof SysMenu)
       menus.add((SysMenu)r);
});

jdk17后的写法:

复制代码
List<?> rows = selectMenuList(menu, getUserId()).getRows();
List<SysMenu> menus =  new ArrayList<>();
rows.forEach(r->{
   if (r instanceof SysMenu sysMenu)
       menus.add(sysMenu);
});

Record关键字-创建不可变类

record用于创建不可变的数据类。在这之前如果你需要创建一个存放数据的类,通常需要先创建一个Class,然后生成构造方法、getter、setter、hashCode、equals和toString等这些方法,或者使用Lombok来简化这些操作。而record就基本相当于实体类+Lombok的效果,但是 record 是不支持继承的,适合数据量少且不复杂的情况下使用。

创建一个实体类 Pon:

复制代码
@Data
@AllArgsConstructor
public class Pon {
    private String name;
    private String nickName;
    private Integer addrNum;
}

然后进行一些简单的测试

复制代码
public class JunitTest {
    @Test
    void test() {

        Pon pon = new Pon("张三", "小三", 65220);
        System.out.println(pon.getName());
        System.out.println(pon.getNickName());
        System.out.println(pon.getAddrNum());
    }
}

输出:

复制代码
张三
小三
65220

以下是使用record代替前面的Pon类,record的属性不需要去定义,直接通过参数的形式设置。

复制代码
public record RecordPon(String name, String nikName, Integer addrNum) {
  
}

接下来对RecordPon进行一些简单的测试

复制代码
public class JunitTest {
    @Test
    void test() {
        RecordPon recordPon = new RecordPon("张三", "小三", 65220);
        System.out.println(recordPon.name());
        System.out.println(recordPon.nikName());
        System.out.println(recordPon.addrNum());
        RecordPon recordPon2 = new RecordPon("李四", "小四", 65220);
        System.out.println(recordPon2.name());
        System.out.println(recordPon2.nikName());
        System.out.println(recordPon2.addrNum());
    }
}

输出:

复制代码
张三
小三
65220
李四
小四
65220

当然了,record 同样也有构造方法,可以在构造方法中对数据进行一些操作,例如:

复制代码
public record RecordPon(String name, String nikName, Integer addrNum) {
    public RecordPon {
        if (Objects.equals(name, ""))
            System.err.println( "name is not null");
    }
}

总结:Lombok 与 record 是不同的工具,服务于不同的目的。此外,Lombok 更加灵活,它可以用于 record 受限的场景。

Stream 新增方法

复制代码
List<String> names = users.stream()
    .map(User::username)
    .filter(n -> n.startsWith("Z"))
    .toList();

List<String> allWords = sentences.stream()
    .mapMulti((line, sink) -> Arrays.stream(line.split(" ")).forEach(sink))
    .toList();

var 结合类型推断

JDK17 用法(JDK10 引入,JDK17 可放心使用)

复制代码
var users = new ArrayList<User>();
var map = new HashMap<String, List<User>>();

示例二

复制代码
@GetMapping("/users")
public List<UserResponse> getUsers() {
    var users = new ArrayList<UserResponse>();
    users.add(new UserResponse("李四", 30, "lisi@example.com"));
    return users;
}

注意事项:var 只能用于局部变量,不能用于字段、方法参数或返回值。保持代码可读性,不要滥用(如 var x = 1; 就没必要)。

不适用场景

  • 声明一个成员变量
  • 声明一个数组变量,并为数组静态初始化(省略new的情况下)
  • 方法的返回值类型
  • 方法的参数类型
  • 没有初始化的方法内的局部变量声明
  • 作为catch块中异常类型
  • Lambda表达式中函数式接口的类型
  • 方法引用中函数式接口的类型

异常处理之try-catch资源关闭

在JDK7 之前,这样处理资源的关闭:

复制代码
@Test
public void test01() {
    FileWriter fw = null;
    BufferedWriter bw = null;
    try {
        fw = new FileWriter("d:/1.txt");
        bw = new BufferedWriter(fw);

        bw.write("hello");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (bw != null) {
                bw.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (fw != null) {
                fw.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

DK7的新特性

在try的后面可以增加一个(),在括号中可以声明流对象并初始化。try中的代码执行完毕,会自动把流对象释放,就不用写finally了

复制代码
try(资源对象的声明和初始化){
    业务逻辑代码,可能会产生异常
}catch(异常类型1 e){
    处理异常代码
}catch(异常类型2 e){
    处理异常代码
}

说明:

  1. 在try()中声明的资源,无论是否发生异常,无论是否处理异常,都会自动关闭资源对象,不用手动关闭了。

  2. 这些资源实现类必须实现AutoCloseable或Closeable接口,实现其中的close()方法。Closeable是AutoCloseable的子接口。Java7几乎把所有的"资源类"(包括文件IO的各种类、JDBC编程的Connection、Statement等接口...)都进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口,并实现了close()方法。

  3. 写到try()中的资源类的变量默认是final 声明的,不能修改。

    举例:

    //举例1
    @Test
    public void test02() {
    try (
    FileWriter fw = new FileWriter("d:/1.txt");
    BufferedWriter bw = new BufferedWriter(fw);
    ) {
    bw.write("hello");
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    //举例2
    @Test
    public void test03() {
    //从d:/1.txt(utf-8)文件中,读取内容,写到项目根目录下1.txt(gbk)文件中
    try (
    FileInputStream fis = new FileInputStream("d:/1.txt");
    InputStreamReader isr = new InputStreamReader(fis, "utf-8");
    BufferedReader br = new BufferedReader(isr);

    复制代码
         FileOutputStream fos = new FileOutputStream("1.txt");
         OutputStreamWriter osw = new OutputStreamWriter(fos, "gbk");
         BufferedWriter bw = new BufferedWriter(osw);
     ) {
         String str;
         while ((str = br.readLine()) != null) {
             bw.write(str);
             bw.newLine();
         }
     } catch (FileNotFoundException e) {
         e.printStackTrace();
     } catch (IOException e) {
         e.printStackTrace();
     }

    }

JDK9的新特性

try的前面可以定义流对象,try后面的()中可以直接引用流对象的名称。在try代码执行完毕后,流对象也可以释放掉,也不用写finally了。

复制代码
A a = new A();
B b = new B();
try(a;b){
    可能产生的异常代码
}catch(异常类名 变量名){
    异常处理的逻辑
}

举例:

复制代码
@Test
public void test04() {
    InputStreamReader reader = new InputStreamReader(System.in);
    OutputStreamWriter writer = new OutputStreamWriter(System.out);
    try (reader; writer) {
        //reader是final的,不可再被赋值
        //   reader = null;

    } catch (IOException e) {
        e.printStackTrace();
    }
}

Helpful NullPointerExceptions

在 Java8 我们如果遇到NPE,通常只会输出将显示NullPointerException发生的行号,但不知道哪个方法调用时产生的null,必须通过调试的方式找到。对于复杂度高的代码来讲就非常耗时,而从Helpful NullPointerExceptions可以在我们遇到NPE时节省一些时间,会准确显示发生NPE的精确位置,比如以下代码会发生一个NPE:

复制代码
public class JunitTest {
    @Test
    void test() {
        String str = null;
        int length = str.length();
    }
}

输出:

密封类 sealed class --类型安全的建模利器

密封类提供了一种控制继承关系的手段,使类层次结构更加严谨且可维护。

密封类是一种特殊的类,它用来表示受限的类继承结构,即一个类只能有有限的几种子类,而不能有任何其他类型的子类。这样可以让我们更好的控制哪些类可以对我定义的类进行扩展,而在这之前一个类要么是可以被extends的,要么是final的任何人都不能继承的,只有这两种选项,没有什么灵活性。首先创建Animal、Dog、Cat类:

复制代码
public abstract class Animal {
     public void put(){
        System.out.println("我是动物");
    }
}

public class Dog extends Animal {

}

在这里这个Animal 是可以被任何一个类继承的,例如:

复制代码
public class JunitTest {
    @Test
    void test() {
        Cat cat = new Cat();
        Dog dog = new Dog();
        Animal animal = cat;
        Animal animal2 = dog;
        class china extends Animal{}
        class usa extends Dog{}
    }
}

除了Dog和Cat类能继承之外,这里创建的china类也能继承Animal类,usa类也继承Dog类。ok这本来没什么问题,就是这个关系有点不正常,国家怎么能继承于动物呢?

在Jdk17中通过密封类可以解决这个问题,主要就这几个关键字:

  • final:不允许被继承

  • sealed:密封类(需要指定哪个类可以扩展它)

  • non-sealed:可以被任何类继承

  • permits :指定哪个类可以继承于我
    现在我们将上面的Animal 类改成密封类,例如:

    public sealed class Animal permits Dog{
    public void put(){
    System.out.println("我是动物");
    }
    }

此处的Animal是密封类,指定了只有Dog类可以继承它

复制代码
public sealed class Dog extends Animal permits DogSon{
}

而此时Dog类也是一个密封类,它也指定了只有DogSon这个类可以继承它。这里做个小测试,创建一个BigDog类并继承Dog:

直接告诉你 密封层次结构中不允许使用"BigDog" ,需要给BigDog指定这个permits ,其他亦是如此,除非你定义成non-sealed。

备注???:密封类的子类必须是 final类、sealed类或non-sealed类,并且 父类和子类 必须在同一个包下

示例二

定义密封类结构

复制代码
public sealed interface Shape permits Circle, Rectangle, Triangle {}

public final class Circle implements Shape {}
public sealed class Rectangle implements Shape permits Square {}
public non-sealed class Triangle implements Shape {}

通过 permits 显式声明子类,编译器将强制检查类型封闭性,配合 switch 使用时更安全。

领域模型中的应用场景

复制代码
public sealed interface Payment permits Alipay, WeChatPay, BankTransfer {}

public final class Alipay implements Payment {}
public final class WeChatPay implements Payment {}
public final class BankTransfer implements Payment {}

虚拟线程(Project Loom)

虚拟线程是Java 17中的一个实验性特性,旨在通过使用用户级线程(fibers)来提高并发性,而不需要内核线程的开销。

复制代码
import java.lang.Thread; // 注意:虚拟线程API可能在JDK中有所不同,具体以实际发布为准。

Thread.Builder builder = Thread.ofVirtual()
                               .name("VirtualThreadExample", 0) // 设置线程名和优先级
                               .unstarted(() -> {
                                   // 线程执行的代码
                                   System.out.println("Virtual thread is running");
                               });
Thread virtualThread = builder.start(); // 启动虚拟线程

Foreign Function Interface(FFI)和 Panama项目中的其他特性(例如 Vector API)

Java 17中的FFI和Panama项目提供了对本地代码的更好支持,包括对本地库的访问和本地内存访问的改进。Vector API允许使用SIMD指令进行更高效的数值计算。

复制代码
import jdk.incubator.vector.*; // 注意:API可能会变化,请查看最新的文档。

VectorSpecies<Integer> vs = IntVector.SPECIES_256; // 使用256位宽的整数向量规格
IntVector v1 = IntVector.fromArray(vs, new int[]{1, 2, 3, 4, 5, 6, 7, 8}, 0); // 创建向量
IntVector v2 = IntVector.fromArray(vs, new int[]{1, 1, 1, 1, 1, 1, 1, 1}, 0); // 创建向量
IntVector result = v1.add(v2); // 向量加法操作
int[] resultArray = result.intoArray(); // 将结果向量转换为数组
System.out.println(Arrays.toString(resultArray)); // 输出结果数组

容器感知(Container Awareness)------ 云原生部署不再被 OOM Kill

需求场景

在 Docker/K8s 中限制容器内存为 512MB,但 JDK8 的 JVM 会按宿主机(比如 16GB)分配堆内存,导致进程被系统杀死。

DK17 自动解决

JDK17 默认启用 -XX:+UseContainerSupport,能正确读取 cgroup 限制,自动调整堆大小。

复制代码
FROM eclipse-temurin:17-jre-alpine
COPY app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

💡 注意事项:确保使用官方 JDK17 镜像(如 Temurin、OpenJDK),避免使用老旧自定义镜像。

API 方法更新

JDK9-JDK11


JDK12新特性:String 实现了 Constable 接口

复制代码
public final class String implements java.io.Serializable, Comparable<String>, CharSequence,Constable, ConstantDesc {

java.lang.constant.Constable接口定义了抽象方法:

复制代码
public interface Constable {
	Optional<? extends ConstantDesc> describeConstable();
}

DK12新特性:String新增方法

String的transform(Function)

复制代码
var result = "foo".transform(input -> input + " bar");
System.out.println(result); //foo bar

或者

复制代码
var result = "foo".transform(input -> input + " bar").transform(String::toUpperCase)
System.out.println(result); //FOO BAR

对应的源码:

复制代码
/**
* This method allows the application of a function to {@code this}
* string. The function should expect a single String argument
* and produce an {@code R} result.
* @since 12
*/
public <R> R transform(Function<? super String, ? extends R> f) {
 return f.apply(this);
}

在某种情况下,该方法应该被称为map()。

复制代码
private static void testTransform() {
	System.out.println("======test java 12 transform======");
	List<String> list1 = List.of("Java", " Python", " C++ ");
	List<String> list2 = new ArrayList<>();
	list1.forEach(element -> list2.add(element.transform(String::strip)
								  .transform(String::toUpperCase)
								  .transform((e) -> "Hi," + e))
				 );
	list2.forEach(System.out::println);
}

======test java 12 transform======
Hi,JAVA
Hi,PYTHON
Hi,C++

jShell命令

ava 终于拥有了像Python 和 Scala 之类语言的REPL工具(交互式编程环境,read - evaluate - print - loop):jShell。以交互式的方式对语句和表达式进行求值。即写即得、快速运行。

利用jShell在没有创建类的情况下,在命令行里直接声明变量,计算表达式,执行语句。无需跟人解释"public static void main(String[] args)"这句"废话"。

使用举例

调出jShell

获取帮助

基本使用

导入指定的包

默认已经导入如下的所有包:(包含java.lang包)

只需按下 Tab 键,就能自动补全代码

列出当前 session 里所有有效的代码片段

查看当前 session 下所有创建过的变量

查看当前 session 下所有创建过的方法

Tips:我们还可以重新定义相同方法名和参数列表的方法,即对现有方法的修改(或覆盖)。

使用外部代码编辑器来编写 Java 代码

从外部文件加载源代码【HelloWorld.java】

复制代码
public void printHello() {
    System.out.println("马上周末了,祝大家周末快乐!");
}
printHello();

使用/open命令调用

退出jShell

GC新特性

GC是Java主要优势之一。 然而,当GC停顿太长,就会开始影响应用的响应时间。随着现代系统中内存不断增长,用户和程序员希望JVM能够以高效的方式充分利用这些内存, 并且无需长时间的GC暂停时间。

G1 GC

JDK9以后默认的垃圾回收器是 G1GC

JDK10 : 为G1提供并行的 Full GC

G1最大的亮点就是可以尽量的避免full gc。但毕竟是"尽量",在有些情况下,G1就要进行full gc了,比如如果它无法足够快的回收内存的时候,它就会强制停止所有的应用线程然后清理。

在Java10之前,一个单线程版的标记-清除-压缩算法被用于full gc。为了尽量减少full gc带来的影响,在Java10中,就把之前的那个单线程版的标记-清除-压缩的full gc算法改成了 支持多个线程 同时full gc。这样也算是减少了full gc所带来的停顿,从而提高性能。

可以通过-XX:ParallelGCThreads参数来指定用于并行GC的线程数。

JDK12:可中断的 G1 Mixed GC

JDK12:增强G1,自动返回未用堆内存给操作系统

Shenandoah GC

JDK12:Shenandoah GC:低停顿时间的GC

Shenandoah 垃圾回收器是 Red Hat 在 2014 年宣布进行的一项垃圾收集器研究项目 Pauseless GC 的实现,旨在针对 JVM 上的内存收回实现低停顿的需求。

据 Red Hat 研发 Shenandoah 团队对外宣称,Shenandoah 垃圾回收器的暂停时间与堆大小无关 ,这意味着无论将堆设置为 200 MB 还是 200 GB,都将拥有一致的系统暂停时间>,不过实际使用性能将取决于实际工作堆的大小和工作负载。

Shenandoah GC 主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关 等。

这是一个实验性功能,不包含在默认(Oracle)的OpenJDK版本中。

Shenandoah开发团队在实际应用中的测试数据:

JDK15:Shenandoah垃圾回收算法转正

Shenandoah垃圾回收算法终于从实验特性转变为产品特性,这是一个从 JDK 12 引入的回收算法,该算法通过与正在运行的 Java 线程同时进行疏散工作来减少 GC 暂停时间。Shenandoah 的暂停时间与堆大小无关,无论堆栈是 200 MB 还是 200 GB,都具有相同的一致暂停时间。

Shenandoah在JDK12被作为experimental引入,在JDK15变为Production;之前需要通过-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC来启用,现在只需要-XX:+UseShenandoahGC即可启用

革命性的 ZGC

JDK11:引入革命性的ZGC

ZGC,这应该是JDK11最为瞩目的特性,没有之一。

ZGC是一个并发、基于region、压缩型的垃圾收集器。

ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%。 将来还可以扩展实现机制,以支持不少令人兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。

JDK13:ZGC:将未使用的堆内存归还给操作系统

JDK14:ZGC on macOS和windows

JDK14之前,ZGC仅Linux才支持。现在mac或Windows上也能使用ZGC了,示例如下

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

ZGC与Shenandoah目标高度相似,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

JDK15:ZGC 功能转正

ZGC是Java 11引入的新的垃圾收集器,经过了多个实验阶段,自此终于成为正式特性。

但是这并不是替换默认的GC,默认的GC仍然还是G1;之前需要通过-XX:+UnlockExperimentalVMOptions、-XX:+UseZGC来启用ZGC,现在只需要-XX:+UseZGC就可以。相信不久的将来它必将成为默认的垃圾回收器。

ZGC的性能已经相当亮眼,用"令人震惊、革命性"来形容,不为过。未来将成为服务端、大内存、低延迟应用的首选垃圾收集器。

怎么形容Shenandoah和ZGC的关系呢?异同点大概如下:

  • 相同点:性能几乎可认为是相同的
  • 不同点:ZGC是Oracle JDK的,根正苗红。而Shenandoah只存在于OpenJDK中,因此使用时需注意你的JDK版本

JDK16:ZGC 并发线程处理

在线程的堆栈处理过程中,总有一个制约因素就是safepoints。在safepoints这个点,Java的线程是要暂停执行的,从而限制了GC的效率。

回顾:

我们都知道,在之前,需要 GC 的时候,为了进行垃圾回收,需要所有的线程都暂停下来,这个暂停的时间我们称为 Stop The World。

而为了实现 STW 这个操作, JVM 需要为每个线程选择一个点停止运行,这个点就叫做安全点(Safepoints)。

ZGC的并发线程堆栈处理可以保证Java线程可以在GC safepoints的同时可以并发执行。它有助于提高所开发的Java软件应用程序的性能和效率。

相关推荐
丶小鱼丶3 分钟前
并发编程之【优雅地结束线程的执行】
java
市场部需要一个软件开发岗位8 分钟前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
忆~遂愿12 分钟前
GE 引擎进阶:依赖图的原子性管理与异构算子协作调度
java·开发语言·人工智能
MZ_ZXD00117 分钟前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东19 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology24 分钟前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble28 分钟前
springboot的核心实现机制原理
java·spring boot·后端
人道领域37 分钟前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七1 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
CodeToGym1 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel